← Docs

Recipe: SKIP LOCKED job queue pattern

Build a concurrent job queue where workers claim rows without blocking each other. Uses PostgreSQL's SKIP LOCKED to eliminate contention.

Schema

CREATE TABLE job_queue (
  id         BIGSERIAL PRIMARY KEY,
  payload    JSONB NOT NULL,
  status     TEXT NOT NULL DEFAULT 'pending',
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_job_queue_status
  ON job_queue (status, created_at)
  WHERE status = 'pending';

Claim query

WITH next_job AS (
  SELECT id
  FROM job_queue
  WHERE status = 'pending'
  ORDER BY created_at
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE job_queue
SET status = 'processing'
FROM next_job
WHERE job_queue.id = next_job.id
RETURNING job_queue.*;

Worker loop

async function worker() {
  while (true) {
    const job = await db.oneOrNone(CLAIM_QUERY);
    if (!job) {
      await sleep(1000);
      continue;
    }
    await processJob(job);
    await db.none(
      'DELETE FROM job_queue WHERE id = $1',
      job.id
    );
  }
}

Why it works

  • No row-level contention — locked rows are invisible to other workers.
  • Linear horizontal scaling — add workers, throughput increases.
  • No orchestration — Postgres is the queue broker.