A production-style background job queue built with NestJS, TypeScript, PostgreSQL, and Docker.
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Client │ HTTP │ API Server │ SQL │ PostgreSQL │
│ (curl/app) │───────▶│ (NestJS REST) │───────▶│ (jobs) │
└──────────────┘ └──────────────────┘ └──────┬───────┘
│
│ poll
▼
┌──────────────┐
│ Worker │
│ (polling) │
└──────────────┘
- Client sends a
POST /jobsrequest to the API. - API inserts a new row into the
jobstable with statuspending. - Worker polls the database every few seconds.
- Worker atomically locks a pending job using
SELECT … FOR UPDATE SKIP LOCKED. - Worker processes the job:
- Success → status set to
completed,processed_atrecorded. - Failure → if
attempts < max_attempts, status reset topendingwith a back-off delay; otherwise status set tofailed.
- Success → status set to
| Status | Description |
|---|---|
pending |
Waiting to be picked up by a worker |
processing |
Currently being processed |
completed |
Successfully processed |
failed |
Permanently failed after exhausting retries |
src/
main.ts # API entry point
worker-main.ts # Worker entry point
app.module.ts # API root module
database/
database.module.ts # Global database module
postgres.service.ts # PostgreSQL connection pool & schema
jobs/
jobs.module.ts # Jobs feature module
jobs.controller.ts # REST endpoints for jobs
jobs.service.ts # Job creation & querying logic
worker/
worker.module.ts # Worker feature module
worker.service.ts # Polling, locking, processing, retry logic
scripts/
init.sql # Database schema (used by docker-compose)
start-api.sh # Convenience script to run the API locally
start-worker.sh # Convenience script to run the worker locally
# Start PostgreSQL, API, and Worker
docker-compose up --build
# The API will be available at http://localhost:3000- Node.js ≥ 18
- PostgreSQL running locally (or via
docker-compose up postgres)
# Install dependencies
npm install
# Copy and adjust environment variables
cp .env.example .env
# Start PostgreSQL only (optional — if you don't have a local instance)
docker-compose up -d postgres
# Start the API server
npm run start:dev
# In a separate terminal, start the worker
npm run start:worker:devCREATE TABLE jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL DEFAULT 3,
available_at TIMESTAMP NOT NULL DEFAULT NOW(),
processed_at TIMESTAMP,
failed_at TIMESTAMP,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);The schema is automatically created on application startup and also provided in scripts/init.sql for the Docker PostgreSQL container.
POST /jobs
Content-Type: application/json
Body:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | Job type identifier |
payload |
object | yes | Arbitrary JSON payload |
max_attempts |
number | no | Max retry attempts (default: 3) |
available_at |
string | no | ISO timestamp for delayed jobs |
GET /jobs?status=pending&limit=50&offset=0
GET /jobs/:id
curl -X POST http://localhost:3000/jobs \
-H "Content-Type: application/json" \
-d '{
"type": "send_email",
"payload": {
"to": "user@example.com",
"subject": "Welcome!",
"body": "Thanks for signing up."
}
}'curl -X POST http://localhost:3000/jobs \
-H "Content-Type: application/json" \
-d '{
"type": "generate_report",
"payload": {
"reportName": "monthly-sales",
"month": "2026-03"
}
}'curl -X POST http://localhost:3000/jobs \
-H "Content-Type: application/json" \
-d '{
"type": "failing_job",
"payload": { "reason": "testing retries" },
"max_attempts": 3
}'curl -X POST http://localhost:3000/jobs \
-H "Content-Type: application/json" \
-d '{
"type": "send_email",
"payload": { "to": "delayed@example.com", "subject": "Delayed" },
"available_at": "2026-03-13T16:30:00.000Z"
}'curl http://localhost:3000/jobscurl "http://localhost:3000/jobs?status=pending"curl http://localhost:3000/jobs/<JOB_UUID>| Script | Description |
|---|---|
npm run start |
Start API (compiled) |
npm run start:dev |
Start API in watch mode |
npm run start:prod |
Start API from dist/ |
npm run start:worker |
Start worker from dist/ |
npm run start:worker:dev |
Start worker with ts-node |
npm run build |
Compile TypeScript |
./scripts/start-api.sh |
Shell script to run API via ts-node |
./scripts/start-worker.sh |
Shell script to run worker via ts-node |
| Variable | Default | Description |
|---|---|---|
POSTGRES_HOST |
localhost |
PostgreSQL host |
POSTGRES_PORT |
5432 |
PostgreSQL port |
POSTGRES_USER |
jobqueue |
PostgreSQL user |
POSTGRES_PASSWORD |
(required) | PostgreSQL password |
POSTGRES_DB |
jobqueue |
PostgreSQL database name |
API_PORT |
3000 |
Port for the REST API |
WORKER_POLL_INTERVAL_MS |
3000 |
Worker polling interval (ms) |
WORKER_RETRY_BASE_SECONDS |
10 |
Base delay for retry backoff |
WORKER_RETRY_MAX_SECONDS |
3600 |
Cap for retry backoff |
# Unit tests (no database — services are tested with mocked Postgres)
npm test
# End-to-end (needs PostgreSQL; enqueues a job over HTTP and asserts the
# worker drives it to 'completed'). Skipped automatically when no DB is set.
docker compose up -d postgres
RUN_DB_E2E=1 \
POSTGRES_HOST=localhost POSTGRES_PORT=5432 \
POSTGRES_USER=jobqueue POSTGRES_PASSWORD=jobqueue POSTGRES_DB=jobqueue \
npm run test:e2eCI runs both suites on every push — see
.github/workflows/ci.yml.
MIT — see LICENSE.