Backend distributed job system built with TypeScript, Node.js, PostgreSQL, Redis, and Docker.
The goal is not to recreate BullMQ feature-for-feature. The goal is to understand the engineering problems behind background processing: safe job claiming, worker coordination, retries, scheduling, observability, and deployment.
The codebase is in Stage 2 of the roadmap.
- PostgreSQL stores durable job metadata and results.
- The worker currently claims jobs from PostgreSQL with an atomic compare-and-set pattern.
- Redis-based queue coordination is the next step and is documented in the local Stage 2 checklist.
flowchart LR
Client[Client] --> API[REST API]
API --> PG[(PostgreSQL)]
API --> R[(Redis Queue)]
R --> W1[Worker 1]
R --> W2[Worker 2]
R --> WN[Worker N]
W1 --> PG
W2 --> PG
WN --> PG
PostgreSQL is the source of truth for job state, while Redis coordinates pending work between workers.
Current job states:
stateDiagram-v2
[*] --> QUEUED
QUEUED --> PROCESSING
PROCESSING --> COMPLETED
PROCESSING --> FAILED
Planned later-stage states and behavior will add retries, delayed execution, and recovery for crashed workers.
- TypeScript + Bun for the API and workers
- Express for the API server
- PostgreSQL for durable job data
- Redis for queue coordination
- Prisma for schema and database access
- Turborepo for workspace orchestration
apps/api: job submission and job status APIapps/worker: background worker processpackages/db: Prisma schema and shared database clientpackages/types: shared typespackages/uiandapps/web: starter frontend packages from the monorepo template
- Bun 1.3+
- PostgreSQL
- Redis
Set these values for the API and worker:
DATABASE_URLREDIS_URL
bun installFrom the database package:
cd packages/db
bun run db:generate
bun run db:migratecd apps/api
bun run index.tscd apps/worker
bun run index.tsFor Stage 2 testing, run multiple worker terminals or containers side by side.
Current code implements POST /jobs and GET /jobs/:id. The final contract also includes GET /health and the later-stage endpoints described in the project roadmap.
Submit a new job.
Example:
curl -X POST http://localhost:3000/jobs \
-H 'Content-Type: application/json' \
-d '{
"type": "report.generate",
"payload": { "userId": "123" }
}'Expected response:
{ "id": "job-id" }Fetch the current durable job record and its status/result.
Example:
curl http://localhost:3000/jobs/job-idHealth check endpoint for the API process.
The final system is designed around at-least-once job processing.
- Duplicate submissions are handled with idempotency keys in later stages.
- A worker crash before completion can result in a retryable job attempt.
- Exactly-once execution is not guaranteed.
- Handlers should be written to tolerate retries and duplicate attempts.
Use a mix of unit, integration, and system-level checks:
- Submit valid and invalid jobs.
- Fetch queued, processing, completed, and failed jobs.
- Run several worker processes concurrently.
- Kill a worker before and during execution.
- Exercise retry and backoff behavior once Stage 3 is implemented.
- Restart API, worker, PostgreSQL, and Redis to confirm durable state behavior.
When measuring throughput, keep the workload and machine constant and compare worker counts under the same job payload and handler cost.
Record:
- worker count
- batch size
- total completion time
- throughput in jobs/sec
- failures or retries
- machine and runtime configuration
Template:
| Workers | Jobs | Total Time (s) | Throughput (jobs/s) | Failures |
|---|---|---|---|---|
| 1 | ||||
| 2 | ||||
| 4 |
Measured on a batch of 20 jobs using the current test handler.
| Workers | Jobs | Total Time (s) | Throughput (jobs/s) | Failures |
|---|---|---|---|---|
| 1 | 20 | 20.95 | 0.95 | 0 |
| 2 | 20 | 10.50 | 1.90 | 0 |
| 4 | 20 | 5.63 | 3.55 | 0 |
Observed scaling:
- 2 workers achieved about 2x the throughput of 1 worker.
- 4 workers achieved about 3.7x the throughput of 1 worker.
- Execution time roughly halved as worker count doubled.
These results show that the current worker model scales horizontally for this workload, while PostgreSQL remains the durable source of truth for job state.
- Redis queue coordination is still being introduced.
- Crash recovery with visibility timeouts is not complete yet.
- Retries and exponential backoff are planned for the next stage.
- The worker currently exits when the queue is empty instead of running as a long-lived poller.
- Add Redis blocking queue consumption and multi-worker coordination.
- Add retry limits, exponential backoff, and dead-letter handling.
- Add job scheduling and priority.
- Add idempotency keys for duplicate submission protection.
- Add worker heartbeats and queue metrics.
- Add Docker Compose for the full stack.
By the end, this repository should demonstrate a complete distributed queue system that another developer can clone, run, submit jobs to, observe across multiple workers, and reason about from the README alone.