Yes: PostgreSQL can serve as a durable job queue when your application already relies on it and jobs need to be created transactionally with business data. Have each worker claim a job by selecting, locking, and updating it in one statement. For priority-aware scheduling that favors older eligible work within each priority, use ORDER BY priority DESC, available_at ASC, id ASC.
When one PostgreSQL table is enough
A jobs table can hold both queued work and its progress through the system. That is especially useful when creating a job must commit or roll back with an application change—for example, recording an order and enqueueing the follow-up task in the same transaction. PostgreSQL then remains the source of truth for both the business record and the work it triggers.
As an Amazon Associate I earn from qualifying purchases.
A queue table is not a separate message broker: your application must define and operate the job lifecycle, including claims, retries, recovery, and cleanup. Whether that trade-off is right depends on transaction coupling, delivery semantics, throughput and latency needs, retry and dead-letter requirements, operational burden, and whether work must fan out across services.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →What the three ordering terms mean
Use an explicit ordering policy in the claim query:
#1 Best Overall
ORDER BY priority DESC, available_at ASC, id ASC
priority DESCtries higher numeric priorities first.available_at ASCfavors the job that became eligible earlier among jobs with the same priority. It can also represent scheduled work: a future job is not claimable until its availability time arrives.id ASCsupplies a stable, unique tie-breaker when priority and availability time match. Makeida unique key, such as a primary key.
This is priority-aware preference, not a promise that every job runs in strict global timestamp order. Because priority sorts first, lower-priority work can wait behind higher-priority work. Under concurrency, a locked row can also be skipped, so a worker may claim a later eligible row instead.
If you want FIFO within each priority
You will also see ORDER BY priority DESC, created_at ASC, id ASC. That sorts by creation time within a priority, rather than by eligibility time. Prefer available_at when jobs may be delayed or scheduled; prefer created_at when the intended policy is arrival order. Bassam Ismail’s example describes priority ordering with FIFO within each priority: PostgreSQL as a job queue.
Rank #2
Claim jobs atomically with SKIP LOCKED
A separate read followed by an update is unsafe: two workers can read the same queued row before either has changed its status. PostgreSQL’s FOR UPDATE SKIP LOCKED lets concurrent consumers avoid waiting on rows another transaction has locked. Its result is intentionally inconsistent, so it is suitable for queue consumers—not general-purpose reporting. See the PostgreSQL 16 documentation.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Use a locking subquery or CTE and update the selected row in the same statement:
Rank #3
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'queued'
AND available_at <= now()
ORDER BY priority DESC, available_at ASC, id ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE jobs AS j
SET status = 'running',
locked_by = $1,
locked_at = now()
FROM next_job
WHERE j.id = next_job.id
RETURNING j.*;
Here, $1 is the worker identifier. The statement selects one due job, locks it, changes its state and records ownership, then returns the claimed row. If no eligible unlocked row is available, it returns no row. Prisma’s walkthrough uses the same essential shape: lock an eligible row in the inner query and mark it running with the outer update in one atomic statement: You don’t need a job queue—Postgres already has SKIP LOCKED.
Keep the claim transaction short
Commit the claim before doing lengthy application work. The update records ownership; it does not require holding a database row lock throughout the job handler. Long-running work inside the claim transaction keeps locks open and can make the database a bottleneck, undermining the benefit of skipping locked rows.
Plan for retries and worker crashes
SKIP LOCKED prevents two workers from claiming the same row at the same moment. It does not guarantee exactly-once execution. If a worker crashes after its claim commits, the row can remain marked running even though the work did not finish. The practical contract is at-least-once execution: a recovery process needs to detect expired claims and return eligible jobs to the queue, using a lease, heartbeat, or reaper.
- Make handlers idempotent so a retry does not duplicate an irreversible side effect.
- Track attempts and set a retry budget so a persistently failing job does not loop forever.
- Define how abandoned running jobs are recognized and recovered;
locked_atandlocked_byprovide useful claim metadata, but the recovery policy belongs to the application.
For more on this execution caveat, see Bassam Ismail’s queue implementation discussion.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Index the claim workload and inspect its plan
Claims repeatedly search the queued, due working set and return rows in a defined order. An index aligned with the status filter and ordering can reduce scans through completed history. For example, this partial index is a starting point to test against the query and data distribution:
CREATE INDEX jobs_queued_claim_idx
ON jobs (priority DESC, available_at ASC, id ASC)
WHERE status = 'queued';
Do not assume the index guarantees a particular plan or speed: inspect the claim with EXPLAIN and measure it on your workload. Indexes also add write and maintenance cost as jobs are inserted and their statuses change. Bassam Ismail discusses the trade-off between an aligned index, a smaller working set, and the cost of maintaining broad indexes in the queue implementation example.
Percona Community reported a 1.68 ms median claim time at 16 workers in its particular 2026 workflow-engine benchmark. That is one benchmark result, not a general PostgreSQL capacity figure: schema, indexes, transaction duration, hardware, workload, and PostgreSQL version all affect performance. Percona Community’s benchmark
Free tools Windows power users keep installed
One-click scans. No signup required.
Decide whether the database queue fits
- Good fit: the application already uses PostgreSQL, and creating a job should commit atomically with application data.
- Design it carefully: workers need concurrent claims, priority policy, retries, crash recovery, and operational monitoring. The table gives you persistence; your application supplies the lifecycle and delivery behavior.
- Compare alternatives: if cross-service fan-out, retry or dead-letter workflows, delivery semantics, throughput, latency, or reduced operational burden are central, evaluate a dedicated queue against those requirements rather than choosing by habit.
There is no universal jobs-per-second promise for this pattern. Establish capacity with the actual schema, claim plan, worker count, transaction lengths, and lock or claim latency of your workload.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




