Skip to content
Back to skills

Set Up Background Jobs

ASecurity

Use when a backend must do work outside the request (email, webhooks, imports, schedules) or publish events reliably — adds a Postgres-backed queue (pg-boss, River, procrastinate), the transactional outbox, idempotent handlers, retries with dead letter, cron jobs.

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 10, 2026
developmentpythongobashsqlnextjsnodefastapidockergitapi

Works with

  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned October 10, 2026

npx -y skills add VelimirMueller/lab-claude-skills --skill set-up-background-jobs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Set Up Background Jobs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Set Up Background Jobs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-set-up-background-jobs/badge)](https://www.skillsdirectory.com/skills/velimirmueller-set-up-background-jobs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: set-up-background-jobs
description: Use when a backend must do work outside the request (email, webhooks, imports, schedules) or publish events reliably — adds a Postgres-backed queue (pg-boss, River, procrastinate), the transactional outbox, idempotent handlers, retries with dead letter, cron jobs.
---

# Set Up Background Jobs

Rules and rationale: [jobs-patterns.md](jobs-patterns.md). Code per track and migrations: [jobs-track-patterns.md](jobs-track-patterns.md). Database seam and transactions: [set-up-database](../set-up-database/SKILL.md). Layer paths: [service-layout.md](../_shared/service-layout.md). Versions: [stack-versions.md](../_shared/stack-versions.md).

Default: a queue that lives in the Postgres you already run. A job is inserted in the same transaction as the write that causes it, so both exist or neither does.

## 1. Audit (change nothing)

```bash
cat .claude/stack-profile.md ~/.claude/stack-profile.md 2>/dev/null     # backend.track, database.{engine,orm,host}, observability, hosting
ls src/platform/queue.ts internal/transport/jobs src/*/transport/jobs.py src/worker.ts cmd/*/worker* 2>/dev/null
grep -rnE "\"(bullmq|bull|agenda|pg-boss|graphile-worker|bee-queue)\"" package.json 2>/dev/null
grep -nE "riverqueue|asynq|machinery" go.mod 2>/dev/null
grep -nE "celery|rq|arq|dramatiq|procrastinate|taskiq" pyproject.toml requirements*.txt 2>/dev/null
grep -rnE "setTimeout\(|setInterval\(|time\.AfterFunc|asyncio\.create_task|BackgroundTasks|go func\(" src internal 2>/dev/null | head   # work done after the response, unrecorded
grep -rniE "redis|sqs|nats|rabbit|kafka" compose.yaml docker-compose.yml .env.example 2>/dev/null | head
```

Record: fire-and-forget work in a handler (lost on restart), cron implemented in an OS crontab or `setInterval`, an existing broker and who runs it, writes followed by a publish in the same function (dual write).

## 2. Decide

- No queue, no outbox: full setup (steps 3 to 7).
- A Postgres-backed queue exists: add only the missing parts (retry config, dead letter, idempotency, retention, outbox).
- Redis or a broker exists and is already operated by the team: keep it for existing flows; use the outbox so new events leave Postgres reliably; do not add a second queue system without a reason.
- `backend.track: supabase` with no own server: `pgmq` and `pg_cron` extensions or Edge Function triggers apply; see [set-up-supabase](../set-up-supabase/SKILL.md). Next.js on serverless hosting cannot run a long-lived worker: use the host's cron plus a separate worker service.
- Everything present and step 7 passes: report "already in place" and stop.

Graduate beyond Postgres only on the triggers in [jobs-patterns.md](jobs-patterns.md), section "When to leave Postgres".

## 3. Detect track

| `backend.track` | Queue | Why this one |
|---|---|---|
| `hono` | `pg-boss` | Typed, dead-letter queues, backoff, tz-aware cron, Drizzle adapter; 80 releases in 12 months, latest 2026-10-08. `graphile-worker` (0.x, 5 releases) is the alternative when jobs are enqueued from SQL triggers |
| `go` | `River` | Go-native, `InsertTx` with `pgx`, unique jobs, periodic jobs, typed args; v0.49 active |
| `fastapi` | `procrastinate` | Async, psycopg 3, can enlist in the request's transaction, periodic tasks; 3.10 active. `pgqueuer` 1.5 is the younger alternative |

All three need a **direct** Postgres connection for workers (no transaction-mode pooler): they use `LISTEN/NOTIFY` and locks that a pooler drops.

## 4. Install only what is missing

```bash
pnpm add pg-boss @opentelemetry/api               # hono: pg-boss imports @opentelemetry/api at runtime (peer)
go get github.com/riverqueue/river github.com/riverqueue/river/riverdriver/riverpgxv5   # go: v0.49 needs Go 1.26+
uv add procrastinate                               # fastapi (psycopg[binary,pool] is already installed: scaffold and set-up-database ship it)
```

If the package manager skips peers (npm `--legacy-peer-deps`), install `@opentelemetry/api` by hand: without it `import 'pg-boss'` throws `ERR_MODULE_NOT_FOUND`.

## 5. Generate the seams

1. Queue seam, one file owns the vendor: TS `src/platform/queue.ts`, Go `internal/transport/jobs/`, Python `src/<pkg>/platform/queue.py`. A typed registry of job names and payloads lives there, nowhere else.
2. Consumer (a non-HTTP transport): TS `src/transport/jobs/*.ts`, Go `internal/transport/jobs/jobs.go`, Python `src/<pkg>/transport/jobs.py`. A handler parses the payload, calls one service method, returns or throws. No business rule inside.
3. Producers call the seam with the open transaction handle (`enqueue(tx, ...)`, `InsertTx`, `configure(connection=...)`). Code per track in [jobs-track-patterns.md](jobs-track-patterns.md).
4. Outbox, only for events that leave the service (broker, webhook, another system): `outbox` table, relay job, `Publisher` interface. A job for this service's own work needs no outbox: the queue row is the outbox.
5. Queue settings, one place: retry limit and backoff, expiry, dead-letter queue, retention. Defaults and why: [jobs-patterns.md](jobs-patterns.md).
6. Schedules: declared in code next to the queue config, with an explicit time zone (`UTC` unless a business rule says otherwise).
7. Schema install belongs to the migration step ([migration-patterns.md](../set-up-database/migration-patterns.md)), not to app startup: `pg-boss migrate` (set `migrate: false` at runtime), River SQL dumped into goose with `river migrate-get`, procrastinate schema inside an Alembic revision.

## 6. Wire

- Worker is a separate entry point of the same image (`node dist/worker.mjs`, `cmd/<svc>/worker`, `procrastinate worker`), scaled and deployed apart from the API. A tiny service may run the worker in the API process; then give the pool enough connections for both.
- Graceful shutdown: on SIGTERM stop fetching, wait for running jobs up to a timeout shorter than the platform's kill timeout, then exit.
- Config via [config.md](../_shared/config.md): `DATABASE_URL` (direct URL for the worker), concurrency, timeouts. Runtime role grants: [jobs-track-patterns.md](jobs-track-patterns.md).
- Observability: log job name, id, attempt and outcome with the trace id ([logging-contract.md](../../core/_shared/logging-contract.md)); alert on dead-letter growth and on the oldest ready job's age ([observability.md](../../core/_shared/observability.md)).
- Cleanup: retention for finished jobs and a scheduled purge of the idempotency table.

## 7. Verify

```bash
# 1. atomicity: enqueue inside a transaction that rolls back -> no job
# 2. retries: a handler that throws runs again, with growing delay; after the limit the job is in the dead-letter state
# 3. idempotency: run one job twice on purpose -> one external effect
# 4. schedule: next run is listed in the queue's schedule table with the right time zone
```

Per track the exact commands are in [jobs-track-patterns.md](jobs-track-patterns.md), section "Verify". Pass criteria: a rolled-back enqueue leaves zero jobs; a failing handler ends in the dead-letter state (`dead-letter` queue, `discarded`, `failed`); the second run of the same job id changes nothing; `SELECT` on the queue table shows the schedule.

## References
- [jobs-patterns.md](jobs-patterns.md): why Postgres first, outbox, idempotency, retries, schedules, retention, graduation.
- [jobs-track-patterns.md](jobs-track-patterns.md): pg-boss, River, procrastinate code and install steps.
- [../design-http-api/api-contract-patterns.md](../design-http-api/api-contract-patterns.md): idempotency keys on the HTTP side.

Files in this skill

  • SKILL.md7.4 KB
  • jobs-patterns.md13 KB
  • jobs-track-patterns.md21.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.