← all notes

A deliberately small job queue

A database table can be enough when the workload is modest and ownership is clear.

A side project needed delayed jobs, retries, and at-least-once delivery. It did not need another service. PostgreSQL was already the durable center of the application, so the queue became a table.

Claim work atomically

select id from jobs
where run_at <= now() and state = 'ready'
order by run_at, id
for update skip locked
limit 1;

The worker claims and marks a row in a short transaction, then performs the slow work outside it. A lease timestamp allows another worker to recover jobs abandoned after a crash.

Make retries visible

Each failure stores a compact error, increments an attempt counter, and schedules the next run with bounded exponential delay. Permanent failures move to a final state instead of disappearing into logs.

This design is not a universal queue. High fan-out, strict ordering, or huge throughput deserve specialized infrastructure. For a few jobs per second, keeping state beside the business transaction removes an entire class of dual-write problems.