"PostgreSQL as a Job Queue: SKIP LOCKED Workers Without Redis"
"postgresql skip locked job queue — build a reliable background job queue in Postgres with FOR UPDATE SKIP LOCKED, claim queries, partial indexes, retries, and the transactional outbox pattern. No Redis required."
If your app already runs PostgreSQL, you already own a job queue. The postgresql skip locked job queue pattern lets multiple workers claim rows concurrently with zero coordination and no extra service: each worker atomically locks the next pending job, processes it, and marks it done — all in one row lock. No Redis, no BullMQ, no SQS. Here's the complete build: the table, the claim query, the partial index, retries, and the traps that bite first-timers. Last updated: October 4, 2026 What you'll learn The FOR UPDATE SKIP LOCKED claim query that powers Postgres job queues A jobs table schema with status, attempts, and scheduled-at columns Why a partial index on pending jobs is what keeps this fast at scale How SKIP LOCKED differs from plain FOR UPDATE (and the race it prevents) Retries with exponential backoff and a dead-letter queue The transactional outbox pattern for dual-write safety Postgres Job Queue Without Redis: When It's the Right Call A Postgres queue fits when throughput is in the hundreds of jobs per second (not tens of thousands), jobs are database-adjacent anyway (send an email when a row is created, process an upload), and you'd rather not operate Redis and wire a new failure mode into your infra. You get ACID for free: a job is claimed inside a transaction, so if the worker crashes mid-job, the claim rolls back and another worker picks it up. It's the wrong tool for massive fan-out, sub-100ms scheduling precision, or multi-region routing — that's where dedicated brokers earn their keep. But for typical app-backend background work, Postgres handles it quietly for years. The rule of thumb: keep the queue table small, index only the pending slice, and archive finished rows aggressively. The Jobs Table Schema One row holds…