Recipe: Queue + worker design
A durable job queue backed by Upstash KV with at-least-once delivery, exponential backoff, and dead-letter routing.
Architecture
Jobs are pushed into a sorted set keyed by scheduled epoch. A single-threaded worker polls the next ready slice, claims items atomically via Lua, and executes handlers with a configurable concurrency cap. Completed jobs are removed; failures are retried with jittered backoff or moved to a dead-letter set after N attempts.
Key layout
queue:pending— sorted set (score = run_at)queue:jobs:{jobId}— hash with payload, attempts, statusqueue:dlq— dead-letter setqueue:inflight— set of claimed job IDs
Claim script
-- Lua (atomic claim)
local now = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local jobs = redis.call('ZRANGEBYSCORE',
'queue:pending', 0, now, 'LIMIT', 0, limit)
for _, id in ipairs(jobs) do
redis.call('ZREM', 'queue:pending', id)
redis.call('SADD', 'queue:inflight', id)
end
return jobsWorker loop
The worker runs a tight loop: claim → execute → ack or retry. Concurrency is bounded by a semaphore. Backoff is min(2^n + jitter, 3600) seconds. A heartbeat goroutine periodically re-claims inflight jobs whose lease expired, preventing silent stalls.
Edge cases
- Duplicate delivery is possible — handlers must be idempotent.
- Lease expiry defaults to 30 s; tune per job weight.
- DLQ entries are retained for 7 days then pruned.