When you need background jobs, the usual answer is Redis plus a queue library. But if you already run Postgres, you may not need another service at all. Postgres has everything a reliable job queue needs: row locks, transactions, SKIP LOCKED and LISTEN/NOTIFY.

I use one on my course platform to send hundreds of videos through a rate-limited video API. It's one table and a small worker. This post covers the pattern in general: the table, how workers claim jobs safely, retries, crash recovery, deduplication, waking workers without polling, and when you should reach for something else.

Why a queue in Postgres?

  • One less service. No Redis to run, back up, secure and monitor.
  • Transactional enqueue. Insert the job in the same transaction as the data it's about.

    If the transaction rolls back, the job never existed. With a separate queue, you can save an order and lose its "send receipt" job, or send a receipt for an order that rolled back.

  • You can query it. It's a table. "Which jobs failed today, and why?" is aSELECT .
  • It's fast enough for most apps: thousands of jobs a minute on ordinary hardware.