A distributed background task processing system written in Go, using Redis for job queuing.
- Producer: accepts new tasks via HTTP
POST /enqueue, lists recent jobs viaGET /api/jobswhen Postgres is configured, and enables CORS for the web UI whenALLOWED_ORIGINSis set. - Worker: consumes queued tasks and executes them concurrently; exposes
GET /metrics. - Redis: primary job queue (
RPUSH/BRPOP). - Postgres: stores job rows (
pending→processing→completed/failed) whenDATABASE_URLis set on both producer and worker. - Web UI (
web/): minimal Vite + React dashboard; deploy to Vercel and point it at public producer and worker URLs.
{
"type": "send_email",
"retries": 3,
"payload": {
"to": "someone@example.com",
"subject": "Welcome!"
}
}Notes:
typeis required and controls which handler runs in the worker (send_email,resize_image,generate_pdf).payloadis a free-form key/value object (any fields you want).retriescontrols how many times the worker will retry on failure.attemptsis internal (the worker tracks it automatically).
High level overview
Redis(either installed locally or via Docker)Gov1.22+ (only if you run withgo run)Docker+docker compose(only if you run viadocker compose)
If you already have Redis locally:
redis-serverOr use Docker:
docker run --rm -p 6379:6379 redis:7-alpinego run ./cmd/producerProducer runs on http://localhost:8080.
In another terminal:
go run ./cmd/workerWorker metrics run on http://localhost:8081/metrics.
From web/:
cd web
npm install
npm run devOpen http://localhost:5173. The dev server proxies /enqueue and /api to http://localhost:8080.
For production, set VITE_API_URL and VITE_WORKER_URL (see Deploy Railway and Vercel below).
Start everything with:
docker compose up --buildIf you prefer background mode:
docker compose up --build -dCompose now includes Postgres and sets DATABASE_URL for both services so the UI can list jobs locally.
| Variable | Service | Purpose |
|---|---|---|
REDIS_URL or REDIS_ADDR |
Producer, Worker | Redis connection |
DATABASE_URL |
Producer, Worker | Postgres (job persistence + /api/jobs) |
QUEUE_NAME |
Producer, Worker | Redis list key (default workqueue:jobs) |
ALLOWED_ORIGINS |
Producer, Worker | CORS: * or comma-separated origins (e.g. your Vercel URL) |
PORT |
Producer, Worker | HTTP listen port (Railway sets this automatically) |
WORKER_CONCURRENCY |
Worker | Goroutine consumers (default 2) |
POST /enqueueGET /api/jobs(JSON array; empty ifDATABASE_URLis not set)GET /health
GET /metricsGET /health
Enqueue:
curl -X POST http://localhost:8080/enqueue \
-H "Content-Type: application/json" \
-d "{\"type\":\"send_email\",\"retries\":3,\"payload\":{\"to\":\"user@example.com\",\"subject\":\"hello\"}}"Fetch metrics:
curl http://localhost:8081/metricsSuccess (200 OK):
{
"status": "queued",
"type": "send_email",
"retries": 3,
"queue_name": "workqueue:jobs",
"job_id": "550e8400-e29b-41d4-a716-446655440000"
}job_id is present only when DATABASE_URL is configured on the producer.
Common error responses:
400 Bad Request(invalid JSON body):- Response body:
invalid JSON body
- Response body:
400 Bad Request(typemissing/empty):- Response body:
type is required
- Response body:
400 Bad Request(retriesnegative):- Response body:
retries cannot be negative
- Response body:
500 Internal Server Error(Redis enqueue failed):- Response body:
failed to enqueue task
- Response body:
500 Internal Server Error(Postgres insert failed):- Response body:
failed to persist job
- Response body:
Response (200 OK):
{
"total_jobs_in_queue": 0,
"jobs_done": 1,
"jobs_failed": 0,
"worker_concurrency": 3,
"queue_name": "workqueue:jobs"
}200 OKwith body:ok
