- Go 94%
- Python 6%
- Docker <1%
- Nix <1%
Tasker #
Tasker is a durable task queue service that allows you to asynchronously perform work. It handles the dispatch and delivery of tasks, as well as outcome persistence. Tasks are dispatched to specified HTTP endpoints on which work is performed.
It is written in Go, and it is backed by SQLite.
Running Tasker #
Note
Tasker is available as a Docker image.
By default, Tasker listens on port 8080. In order to persist data, provide a volume mapping to /data.
Tasker will automatically create and migrate an SQLite database if one does not exist.
The image uses Docker's default user (root). To run as another user, set
--user UID:GID (or user in Compose) and ensure that user can write to the
mounted data directory.
To run it via Docker:
docker run -d \
--name tasker \
-p 8080:8080 \
-v ./tasker-data:/data \
kennethnym/tasker
To customize settings, create tasker.yaml and mount it into the container:
docker run -d \
--name tasker \
-p 8080:8080 \
-v ./tasker-data:/data \
-v ./tasker.yaml:/data/tasker.yaml:ro \
kennethnym/tasker
Or use Docker Compose:
services:
tasker:
image: kennethnym/tasker
ports:
- "8080:8080"
volumes:
- ./tasker-data:/data
- ./tasker.yaml:/data/tasker.yaml:ro
restart: unless-stopped
Configuration #
Tasker reads a YAML file: tasker.yaml in the working directory, or the path supplied with --config (tasker --config /etc/tasker.yaml). The Docker image, by default, looks for /data/tasker.yaml, which you can mount to customize settings. If the selected file does not exist, Tasker falls back to defaults.
Omitted fields use the defaults shown below:
logging:
format: json
level: info
access: false
metrics:
enabled: false
http:
listen_address: ":8080"
shutdown_timeout: 10s
database:
path: tasker.db
queue:
max_dispatches_per_second: 10
max_concurrent_dispatches: 20
dispatch_deadline: 5m
max_attempts: 5
min_backoff: 1s
max_backoff: 1m
task_retention: 168h
Please refer to the reference for more details.
Metrics #
Set metrics.enabled: true in tasker.yaml to expose GET /metrics for
Prometheus and Grafana. It uses the HTTP API's listen address and authentication.
Metrics cover queue depth and delay, deliveries, retries, persisted outcomes,
lease recovery, storage errors, HTTP requests, and Go/process health.
See the metrics reference and setup guide, including a sample Prometheus configuration, alert rules, and an importable Grafana dashboard.
Authentication #
Takser supports optional HTTP Basic Auth on inbound and outbound.
Add either or both sections to tasker.yaml:
auth:
username: tasker-client
password_file: /run/secrets/tasker_api_password
outbound_auth:
- origin: https://reports.example.com
username: tasker
password_env: REPORTS_PASSWORD
- origin: http://worker:3001
username: tasker
password_file: /run/secrets/worker_password
When dispatching to a task endpoint, Tasker finds a matching basic auth credentials based on the domain, and adds it to the request header if one is found. The receiver of the task can then validate the provided credentials.
Incoming API requests #
When auth is configured, callers must send its username and password using HTTP Basic Auth. Missing or invalid credentials receive 401 Unauthorized with a WWW-Authenticate challenge. The configured account can create and read all tasks.
Outgoing worker requests #
Tasker selects outbound credentials by the task URL's exact origin: scheme, hostname, and effective port. For example, https://reports.example.com matches every path on that origin, including an explicit port 443, but not HTTP or another port.
Unmatched origins receive no Authorization header. Tasks need no auth field, and inbound credentials are not forwarded to workers. Redirects are not followed.
Password sources #
Each credential entry requires a username and exactly one of password_env or password_file. Supply the referenced variable or mount the secret file inside the Tasker container. Missing, unreadable, or empty secrets fail startup.
Relative secret paths resolve against the configuration file's directory. Credentials load at startup; restart Tasker after rotation. Passwords are not stored in task records.
Use HTTPS outside a trusted private network. Tasker's inbound HTTPS can terminate at a reverse proxy. See the authentication details for hostname normalization and validation rules.
Using Tasker #
A task can be dispatched via POST /tasks. Here is an example task payload:
{
"kind": "send-email",
"url": "https://example.com/workers/send-email",
"input": {
"to": "alice@example.com",
"subject": "Hello"
},
"scheduleTime": "2027-01-01T09:00:00Z",
"dispatchDeadline": "30s"
}
A successful enqueue gives 201 and the following response:
{
"id": "cm12345670000abcd1234efghi",
"kind": "send-email",
"url": "https://example.com/workers/send-email",
"input": {
"to": "alice@example.com",
"subject": "Hello"
},
"status": "queued",
"scheduleTime": "2027-01-01T09:00:00Z",
"dispatchDeadline": "30s",
"attempt": 0,
"maxAttempts": 5,
"result": null,
"createdAt": "2026-10-25T12:00:00Z",
"updatedAt": "2026-10-25T12:00:00Z"
}
Worker delivery #
Tasker sends an HTTP POST to the task's url, with input as the request body and these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Task-Id |
Task ID. |
X-Task-Attempt |
Current attempt number, starting at 1. |
Tasker guarantees at-least-once delivery, but deliveries of the same task can happen more than once. Task executors should take this into consideration.
Result handling #
- Any
2xxresponse marks the task as succeeded. - Non-
2xxresponses, network errors, and timeouts are all considered failures. Tasker will attempt to retry the task with backoff, untilmaxAttemptsis reached. - A valid JSON response body of at most 1 MiB is stored in
result. - Empty, invalid JSON, or oversized response bodies will be discarded and leave
resultasnullwithout failing the task. - Redirects are not followed.
Reference #
Successful responses use Content-Type: application/json. Error responses contain plain text, not JSON.
Configuration fields #
| Field | Type | Default | Description and constraints |
|---|---|---|---|
logging.format |
string | json |
Log format: json or text. Logs are written to stderr. |
logging.level |
string | info |
Minimum severity for runtime and access logs: debug, info, warn, or error (case-insensitive). Go slog level offsets such as info+2 are also accepted. |
logging.access |
boolean | false |
Enable HTTP access logs, including authentication failures. Records include method, route pattern, status, response size, duration, and a generated request ID returned in X-Request-Id. Subject to logging.level; server errors use error level, other requests use info level. |
metrics.enabled |
boolean | false |
Expose GET /metrics on the HTTP listener, protected by the same inbound auth as the task API. See metrics. |
http.listen_address |
string | :8080 |
HTTP server listen address in host:port form. An empty host listens on all interfaces. Must not be empty or whitespace-only. |
http.shutdown_timeout |
duration string | 10s |
Grace period for in-flight HTTP requests during shutdown. Must be positive. |
database.path |
string | tasker.db |
SQLite database filename. Relative paths resolve against the working directory (/data in Docker), not the config file's directory. The parent directory must exist; the database is created and migrated automatically. Must not be empty or whitespace-only. |
queue.max_dispatches_per_second |
number | 10 |
Maximum rate of task dispatches. Fractional values are accepted. Must be positive and finite. |
queue.max_concurrent_dispatches |
integer | 20 |
Maximum number of simultaneous task deliveries. Must be positive. |
queue.dispatch_deadline |
duration string | 5m |
Default timeout for each delivery attempt. A task can override this with the dispatchDeadline request field. Must be positive. |
queue.max_attempts |
integer | 5 |
Maximum delivery attempts per task, including the initial attempt. Must be positive. |
queue.min_backoff |
duration string | 1s |
Minimum retry backoff after a failed delivery. Must be positive. |
queue.max_backoff |
duration string | 1m |
Upper bound for retry backoff. Must be positive and at least queue.min_backoff. |
queue.task_retention |
duration string | 168h |
How long succeeded and failed tasks are retained after finishing before becoming eligible for cleanup. Must be positive. |
Notes:
- All fields are optional.
- Omitted or
nullvalues retain their defaults. - Durations are strings using Go duration syntax, such as
30s,5m, or1h30m; use168hfor seven days.
Task #
Task is returned after a successful task creation and after a GET lookup.
| Field | Type | Description |
|---|---|---|
id |
string | Unique task ID. |
scheduleId |
string | Source cron schedule ID. Omitted for directly submitted tasks. |
kind |
string | Task label. Omitted when empty. |
url |
string | Worker endpoint. |
input |
Any JSON value | Payload sent to the worker. |
status |
string | queued, running, succeeded, or failed. |
scheduleTime |
RFC 3339 timestamp | Scheduled delivery time. Updated when a retry is scheduled. |
dispatchDeadline |
string | Timeout per delivery attempt, e.g. 30s. |
attempt |
integer | Delivery attempt count. Starts at 0. |
maxAttempts |
integer | Maximum delivery attempts, including the initial attempt. |
result |
Any JSON value | Worker’s JSON response, or null when no result is stored. |
lastError |
string | Last delivery error. Omitted when empty. |
createdAt |
RFC 3339 timestamp | When the task was created. |
updatedAt |
RFC 3339 timestamp | When the task was last updated. |
startedAt |
RFC 3339 timestamp | When execution started. Omitted until set. |
finishedAt |
RFC 3339 timestamp | When the task succeeded or permanently failed. Omitted until set. |
POST /tasks #
Enqueues a task for delivery. A successful response means the task has been stored, not that the worker has completed it.
Request:
Send a JSON object with Content-Type: application/json. The request body is limited to 1 MiB.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kind |
string | No | Empty | Optional label for the task. Does not affect delivery. |
url |
string | Yes | — | HTTP or HTTPS endpoint that receives the task. |
input |
Any JSON value | Yes | — | Sent as the worker’s POST body. Can be an object, array, scalar, or null. |
scheduleTime |
RFC 3339 timestamp | No | Current time | Earliest time the task can run. Omitting this makes the task immediately eligible for delivery. |
dispatchDeadline |
Duration string | No | Server setting (5m by default) |
Timeout for each delivery attempt. Must be positive, e.g. 30s or 2m. null is rejected. |
Returns:
| Code | Body | Description |
|---|---|---|
201 Created |
Task | Task stored. The Location header contains /tasks/{id}. |
401 Unauthorized |
unauthorized |
Inbound auth is enabled and credentials are missing or invalid. |
400 Bad Request |
Error message | Invalid JSON, unknown fields, missing required fields, or invalid field values. |
413 Request Entity Too Large |
request body exceeds 1 MiB |
Request body exceeds the size limit. |
500 Internal Server Error |
failed to create task |
Task could not be stored. |
GET /tasks/{id} #
Retrieves the current Task, including its status, delivery attempt count, and result. Uses the same response shape as task creation.
| Path parameter | Type | Description |
|---|---|---|
id |
string | Task ID returned by POST /tasks. |
Returns:
| Code | Body | Description |
|---|---|---|
200 OK |
Task | Current task state. |
401 Unauthorized |
unauthorized |
Inbound auth is enabled and credentials are missing or invalid. |
404 Not Found |
task not found |
No task exists with this ID, or it has been removed by retention cleanup. |
500 Internal Server Error |
failed to find task |
Task could not be read. |
Poll this endpoint until status is succeeded or failed. A failed delivery can return to queued while waiting for another attempt.
Cron schedules #
Create a recurring schedule with POST /schedules:
{
"cron": "0 9 * * MON-FRI",
"kind": "daily-report",
"url": "https://example.com/workers/report",
"input": {"team": "operations"},
"dispatchDeadline": "30s"
}
Expressions have five fields: minute, hour, day of month, month, and day of
week. All times are UTC. Lists, ranges, steps, and month/weekday names are
supported. Sunday is 0; if both day-of-month and day-of-week are restricted,
either may match. Seconds, timezone prefixes, and shortcuts such as @daily
or @every are rejected. Expressions without a future occurrence are rejected.
Schedules have no TTL and remain until explicitly deleted. Each occurrence
creates an ordinary task with its own ID, retries, result, and normal task
retention. Its scheduleId identifies the source schedule, including after
that schedule is deleted. Cron tasks share the existing dispatcher, rate
limits, and concurrency; a backlog can delay execution. Overlapping
occurrences are allowed and retry independently.
The scheduler checks for due schedules at startup and every second. After
downtime, it creates one catch-up task for the stored nextRunAt, then advances
to the first future occurrence. It does not replay every missed occurrence.
Enqueueing and advancing the cursor commit together, preventing duplicate
occurrence creation after a restart or by competing schedulers. HTTP delivery
still has the usual at-least-once guarantee.
Schedule fields #
| Request field | Required | Default | Description |
|---|---|---|---|
cron |
Yes | — | Five-field UTC cron expression. |
url |
Yes | — | Worker HTTP/HTTPS endpoint, validated as for tasks. |
input |
Yes | — | Any JSON value, including null. |
kind |
No | Empty | Task label copied to each occurrence. |
dispatchDeadline |
No | Server setting | Positive duration per attempt; null is rejected. |
enabled |
No | true |
Set to false to pause a schedule; null is rejected. |
Responses include these fields plus id, nextRunAt, maxAttempts,
createdAt, and updatedAt. Times use RFC 3339 UTC. maxAttempts is copied
from the server setting when the schedule is created or replaced. The
deadline is also resolved then, so a restart with different defaults does
not alter stored schedules. For a paused schedule, nextRunAt is an inactive
cursor and does not cause dispatch.
Schedule endpoints #
| Endpoint | Behavior | Success |
|---|---|---|
POST /schedules |
Create a schedule. First occurrence is strictly after creation. | 201, schedule JSON, and Location: /schedules/{id}. |
GET /schedules |
List schedules, including paused schedules, in ID order. | 200, JSON array (empty list is []). |
GET /schedules/{id} |
Retrieve one schedule. | 200, schedule JSON. |
PUT /schedules/{id} |
Replace the full schedule using the creation fields. | 200, updated schedule JSON. |
DELETE /schedules/{id} |
Permanently remove a schedule. | 204, empty body. |
List pagination uses limit (default 100, range 1–1000) and after (the last
ID returned). Continue with GET /schedules?limit=100&after={lastId} until the
response is empty.
To pause or resume, send the full definition with enabled: false or
enabled: true using PUT. Every replacement recalculates nextRunAt from
the current time; resuming skips the paused period. Omitted optional fields
use their creation defaults. Read-only response fields are not accepted in
requests. Updating, pausing, or deleting a schedule leaves tasks already
enqueued unchanged.
These routes use the same inbound authentication as the task API, and generated
tasks use the same outbound authentication. Request bodies have the same 1 MiB
limit and reject unknown fields. Errors return plain text: 400 for invalid
requests, 401 for failed authentication, 404 for a missing schedule, 413
for an oversized body, or 500 for storage failure. Existing databases migrate
automatically on startup.
License #
Tasker is licensed under the MIT License.