ServicesJob Queue

Job Queue

Delayed job scheduling with webhook delivery: hand the server a JSON payload, a URL, and a time, and it POSTs the payload to that URL when the time comes - with per-queue retry policies, exponential backoff, dead letter queues, priority lanes, pause/resume, and runtime statistics.

When to reach for it: delayed webhooks (send a reminder in an hour), retrying calls to a flaky downstream with backoff, spreading bursts of work over time, priority lanes for background work the server executes for you.

When not to: this is a push model - the server calls your endpoint. If your own workers should pull messages off a queue and acknowledge them (SQS-style), that is the Message Queue; if you want to fan out ephemeral messages to whoever is listening right now, with no persistence or retries, that is Pub/Sub.

One service, three CLI commands

The HTTP API is one service, but the CLI splits it by role:

  • snug job-queues - queue manifests: create, configure retries and DLQ, pause/resume, redrive. Takes queue IDs (jq_...).
  • snug jobs - individual jobs: schedule, list, inspect, cancel. Takes job IDs (job_...) and queue names.
  • snug queues - read-only runtime stats (depth, success rate).

Concepts

  • Every queue has a manifest holding its retry policy, DLQ config, default priority, retention, and active flag. The default queue is auto-created on first use; any other queue must be created before you can schedule into it - otherwise 404 queue_not_found.
  • Jobs fire as signed webhook POSTs. A 2xx response counts as success; anything else (or a 30-second timeout) is a failed attempt.
  • Priorities - critical, high, normal, low, bulk - are processing lanes within a queue, not scheduling times.
  • Retries and the DLQ: failed attempts retry with exponential backoff until the attempt budget is spent; then the job is failed and, if the queue has a DLQ, dead-lettered for inspection and redrive.
  • Pause holds delivery, not intake: a paused queue still accepts new jobs; due jobs wait until resume.
  • Jobs are retained, then pruned: terminal jobs (completed, failed, cancelled) are cleaned up after the retention window (7 days by default).

Create and configure a queue

snug job-queues create docs4-emails -d "Docs email queue" \
  --enable-dlq --max-attempts 2 --base-delay-ms 500
snug job-queues list --search docs4
snug job-queues info jq_fvXKFGTtWYGa
snug job-queues update jq_fvXKFGTtWYGa --retention-days 3 --default-priority low
snug job-queues pause jq_fvXKFGTtWYGa    # active: false - delivery stops
snug job-queues resume jq_fvXKFGTtWYGa   # active: true - held jobs fire
snug job-queues delete jq_fvXKFGTtWYGa --force

create returns only {id, name, created_at} - use info for the resolved configuration and live job counts. Names are unique per user (409 queue_already_exists on a duplicate), and deleting a queue with pending jobs fails with 409 queue_has_pending_jobs unless you pass --force.

Schedule a job

snug jobs schedule -w https://example.com/hook -q docs4-emails -d 60 \
  -p high -j '{"to":"user@example.com","template":"welcome"}'

Over HTTP the same call is POST /api/v1/jobs/schedule; response captured live:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"queue":"docs4-emails","delay_seconds":60,"payload":{"to":"user@example.com"},
       "webhook":{"url":"https://example.com/hook"}}' \
  http://localhost:4000/api/v1/jobs/schedule
{
  "status": 201,
  "msg": "Created",
  "data": { "job_id": "job_MApgwfcqfspu", "scheduled_at": 1787898570,
            "queue": "docs4-emails", "status": "scheduled" }
}

delay_seconds must be greater than 0 (400 INVALID_REQUEST otherwise). The API alternatively accepts an absolute scheduled_at unix timestamp, which must be at least 5 seconds in the future; the CLI only exposes --delay. --payload is required - pass '{}' if the job needs no body.

The webhook receives the payload JSON-serialized inside a delivery envelope ({"id": "jobqueue_...", "type": "test.webhook", "message": "<your payload as a string>", ...}), signed with the job's secret - see delivery details.

Batch scheduling

snug jobs schedule-batch -f jobs.json takes a bare JSON array of schedule requests (up to 1000) and returns per-job results - verified with one invalid entry: "success_count": 2, "failure_count": 1, the bad row reported in failed with its array index. Exception: an unknown queue name fails the whole request with 404 queue_not_found and nothing is scheduled.

Inspect and cancel

snug jobs list --queue docs4-emails --status scheduled --limit 20
snug jobs info job_cKKaaVwTgDrM
snug jobs cancel job_cKKaaVwTgDrM
snug jobs cancel-all --queue docs4-emails --force   # your scheduled jobs only

Statuses are scheduled, processing, completed, failed, cancelled. cancel-all is the emergency kill switch for your own scheduled jobs, including jobs parked between retry attempts. Jobs and queues are per-user: another user's info/cancel on your resources fails with 403 FORBIDDEN (reproduced with a second token).

Two operations require a platform admin token: snug jobs admin-cancel-all (bulk-cancel across users, optionally scoped by --user / --queue) and snug jobs cleanup (prune terminal jobs past retention on demand).

Monitor

snug queues stats docs4-emails
snug health job-queue

stats reports queue depth (scheduled), in-flight count, one-hour completed/failed counters, and success rate - shapes and caveats in Reliability. The health check (GET /health/job_queue, no auth) reports Redis connectivity plus scheduler_running and processor_running.

Behaviors and gotchas

Each of these was reproduced live:

  • Scheduling into a nonexistent custom queue: 404 queue_not_found with the hint "Custom queues must be created before use". Only default auto-creates.
  • A queue's default_priority applies only to HTTP requests that omit priority. The CLI always sends one (-p defaults to normal), so a manifest default never takes effect on CLI-scheduled jobs.
  • While paused, a due job stayed scheduled with 0 attempts; on resume it was delivered within seconds.
  • Error-code casing is split: queue-domain failures use lowercase codes (queue_not_found, queue_already_exists, queue_has_pending_jobs), while the job handlers use the shared uppercase codes (INVALID_REQUEST, FORBIDDEN, NOT_FOUND).

Failure handling

Retry budgets, backoff, the dead letter queue, redrive, and stats semantics have their own page: Reliability.

Limits and configuration

Worker pool sizes per priority lane, the scheduler poll interval, retention, cleanup cadence, and stream tuning are the JOB_QUEUE_* variables in the Job Queue CONFIG reference. Batches cap at 1000 jobs; DLQ listing pages cap at 500 entries.

Reference

On this page