Contents

SKIP LOCKED vs CAS in PostgreSQL

Two ways to let many workers claim rows without stepping on each other: the overview

Eight workers poll the same jobs table. Each job must be processed by exactly one of them. Two PostgreSQL tools solve this, FOR UPDATE SKIP LOCKED and compare-and-swap (CAS), and on the same workload one claimed every job it tried for while the other lost most of its attempts.

This is the overview. Each strategy gets its own deep dive: SKIP LOCKED explained and CAS explained .

The Problem

CREATE TABLE jobs (
  id      bigserial PRIMARY KEY,
  status  text NOT NULL DEFAULT 'pending',
  owner   text,
  version int  NOT NULL DEFAULT 0
);

CREATE INDEX jobs_pending ON jobs (id) WHERE status = 'pending';

The obvious claim is two steps, a SELECT then an UPDATE:

SELECT id FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1;
UPDATE jobs SET status = 'running', owner = 'w1' WHERE id = 42;

Between them, another worker can run the same SELECT, get id 42, and run the same UPDATE. Both believe they own job 42. Both strategies close that gap, in opposite ways.

The Two Ideas

FOR UPDATE SKIP LOCKEDCAS
StylePessimistic: lock the row, then change itOptimistic: change it only if it’s still what I saw
Loser’s experienceNever sees the row, takes the next oneWaits briefly, re-checks, updates 0 rows
Needs a retry loopNoYes
Row pickedAny unlocked oneExactly the one you named

The Measured Result

I ran three claim strategies with pgbench on PostgreSQL 16: 20,000 pending jobs, 8 clients, 1,000 claim attempts each.

StrategyClaims succeeded (of 8,000)Wasted attempts
FOR UPDATE SKIP LOCKED8,0000
CAS on the head row (single statement)2,7455,255 (66%)
CAS on a random one of the first ~200 pending rows7,799201 (2.5%)

One run on a laptop, a local socket, fsync=off, no work between claims. It shows the shape (naive CAS degrades with contention, SKIP LOCKED doesn’t), not your production throughput. Raw speed was similar for all three (roughly 9,000 to 12,000 statements per second), which is why the wasted attempts are the number that matters.

Which One

The situationReach forBecause
Many workers draining a job queueSKIP LOCKEDNo contention on the head row, no retry loop
A user edits a record others may edit tooCAS on versionYou must act on that row and detect that it changed
A state transition that must happen once (pending → paid)CAS on statusThe WHERE clause is the guard
Queue throughput matters and you need safe completionBothSKIP LOCKED to claim, CAS on owner and version to finish

They aren’t rivals. SKIP LOCKED decides who gets which row, and CAS makes sure the finish is only accepted from the worker that still owns it.

Next