When we need background jobs in an application, we usually think of Redis, BullMQ, or RabbitMQ.
They're great tools. But they also mean more infrastructure to manage.
What if you're already using PostgreSQL and don't need another service?
There's a simple PostgreSQL feature called SKIP LOCKED that makes this possible.
Let's see how it works.
The problem with background jobs
Imagine you're building an application that sends emails in the background.
You store pending emails in a database table called jobs.
SELECT * FROM jobs
WHERE status = 'pending'
LIMIT 1;
Simple, right?
But what happens when three workers run this query at the same time?
They might all pick the same job and send the same email three times.
That's definitely not what we want.
Meet SKIP LOCKED
PostgreSQL has a neat solution:
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'pending'
ORDER BY created_at, id
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs
SET status = 'processing'
WHERE id = (SELECT id FROM next_job)
RETURNING *;
It looks a little complicated, but the idea is simple.
-
FOR UPDATElocks the selected job so another worker can't claim it. -
SKIP LOCKEDtells other workers to skip locked jobs instead of waiting. -
UPDATE ... RETURNINGmarks the job as processing and returns it.
So if three workers are running:
- Worker 1 picks Job A.
- Worker 2 skips Job A and picks Job B.
- Worker 3 skips both and picks Job C.
Three workers. Three different jobs. No duplicate claims.
And because the job selection and update happen in one statement, the claim is atomic.
Why is this useful?
The biggest benefit is simplicity.
If your application already uses PostgreSQL, you can handle basic background jobs without deploying Redis.
You also get another useful feature: database transactions.
For example, you can save an order and create its background job in the same transaction.
If something fails, both changes can be rolled back.
No worrying about an order being saved while its job never gets queued.
Is it better than Redis?
Not always.
PostgreSQL is a good choice for smaller or moderate background workloads, especially when you're already using it.
Redis with BullMQ is often a better fit when you need advanced scheduling, retries, monitoring, or very high job throughput.
And there's one important catch: SKIP LOCKED only solves the problem of safely claiming jobs.
For production, you still need to handle failed jobs, retries, worker crashes, and duplicate execution after failures.
Libraries like pg-boss and Graphile Worker can help with that.
Final thoughts
We often add new tools without checking what our existing stack can already do.
PostgreSQL's SKIP LOCKED is a perfect example.
It's a small SQL feature that lets multiple workers safely pick different jobs without blocking each other.
It won't replace Redis everywhere.
But for many applications, PostgreSQL might be all you need to get started.
Sometimes, the simplest solution is already sitting in your database.
Top comments (0)