A PostgreSQL worker pool can prevent two workers from claiming the same row at the same instant and still be operationally wrong. A worker can hold a row lock while doing slow work. An old job can be bypassed repeatedly. A committed claim can survive after its worker disappears. A retry can execute business work twice. An apparently FIFO query can stop behaving like strict FIFO as soon as locked rows are skipped.
This playbook tests those correctness questions without reopening the broader “PostgreSQL versus Redis” architecture debate. It is deliberately a deeper correctness-and-fairness follow-up to Refonte Learning’s existing queue coverage, not another argument for choosing a database as a message broker.
The laboratory boundary is intentionally narrow: one disposable PostgreSQL instance, synthetic rows, fixed worker IDs, deterministic ordering fields, local delay only, and no customer data, production databases, real queues, webhooks, email, payments, or other external side effects. No experiment below has been executed for this article, so every “actual observation” field starts as not observed.
Four evidence labels apply throughout. Documented behavior means PostgreSQL explicitly documents the semantic. Engineering inference means a conclusion derived from those semantics and the proposed design. Proposed experiment means something the acceptance team must run. Actual observation means captured evidence from that run; this article does not fabricate it.
The PostgreSQL baseline is the current PostgreSQL 18 documentation: PostgreSQL 18 SELECT documentation (current documentation, undated page; accessed September 22, 2026), Transaction Isolation (current documentation, undated page; accessed September 22, 2026), and Explicit Locking (current documentation, undated page; accessed September 22, 2026). As accessed, PostgreSQL lists 18 as the current supported documentation branch.
Why SKIP LOCKED correctness is different from simple duplicate prevention
FOR UPDATE SKIP LOCKED solves a narrower problem than many queue implementations implicitly assign to it. PostgreSQL documents that SKIP LOCKED causes rows that cannot be locked immediately to be skipped. PostgreSQL also explicitly warns that this produces an inconsistent view of the data and describes the behavior as useful for queue-like tables with multiple consumers rather than for general-purpose consistent reads.
That means “two concurrent workers did not return the same locked row” is only one correctness property. It does not establish FIFO service, starvation freedom, eventual completion, balanced distribution among workers, exactly-once execution, or safe recovery after a committed claim. Those properties either need additional application state or must be defined as testable operational invariants.
A locking SELECT also interacts with transaction lifetime. FOR UPDATE prevents another transaction from modifying or acquiring conflicting row locks on the selected row until the transaction ends, while ordinary non-locking reads are not blocked by row locks. PostgreSQL advises against holding transactions open for long periods.
The broader operational mindset fits Refonte Learning’s Database Administration in 2026: Performance, Security, and Scaling: queue acceptance is database operations work because transaction duration, indexing, monitoring, recovery, and failure evidence matter alongside SQL syntax.
Question | Duplicate-prevention test answers it? | This playbook must answer it? |
Can two workers lock the same row concurrently? | Yes | Yes |
Does an older locked row remain ahead of newer unlocked rows? | No | Yes |
Can an eligible job be skipped repeatedly? | No | Yes |
Does a claim remain recoverable after worker failure? | No | Yes |
Can work execute twice across retry boundaries? | No | Yes |
Are locks held during slow application work? | No | Yes |
Does every finite synthetic job eventually reach a terminal state? | No | Yes |
Does worker-count distribution prove fairness? | No | No |
Engineering inference. SKIP LOCKED should be treated as a contention-handling primitive, not as a fairness contract. The acceptance design therefore separates row-lock correctness, claim-state correctness, job progress, retry behavior, and worker distribution.
Actual observation. None. The table defines questions to prove in the disposable laboratory, not results.
Define worker, job, claim, retry, and fairness invariants
The vocabulary must be frozen before testing. A job is one synthetic row representing one logical unit of work. A worker is a deterministic client identity such as worker-a. A claim is a committed state transition assigning one eligible job to one worker and one claim token. Execution occurs after that transaction commits.
A retry is a later claim attempt for the same logical job after the previous attempt fails, expires, or is explicitly released. An incremented attempt number identifies that new attempt. A duplicate execution means the logical effect is attempted more than once, even when each attempt held a valid claim at a different time.
For this playbook, fairness is operationally defined, not attributed to PostgreSQL. Job-order fairness means that unlocked eligible jobs should follow the test’s total ordering. Starvation freedom means a finite eligible cohort eventually leaves ready after temporary blockers disappear. Worker fairness is weaker: the test records worker distribution but does not demand equal counts.
Invariant | Operational definition | Acceptance evidence |
Unique live claim | One job has at most one current claim token | claim-event history |
Total candidate order | priority DESC, available_at, created_at, id | ordered snapshots |
Claim atomicity | selected row and claim metadata commit together | UPDATE … RETURNING record |
Short claim transaction | transaction ends before synthetic work starts | timestamped worker trace |
Attempt identity | each new claim increments attempts and gets a new token | job/event rows |
Stale-worker fencing | old token cannot complete a newer claim | rejected completion test |
Closed-cohort progress | every finite test job eventually reaches an accepted terminal state | final cohort query |
No fairness overclaim | equal worker shares are not an acceptance requirement | methodology notes |
Engineering inference. The id tie-breaker matters because ORDER BY should be total for deterministic tests. PostgreSQL notes that LIMIT without a sufficiently deterministic ORDER BY can return different subsets; a complete ordering removes an avoidable source of ambiguity from this lab.
The fairness statement should therefore read: given this eligibility predicate, immutable ordering fields, this total ordering, and the absence of a conflicting row lock on the highest-ranked candidate, the test expects that candidate to be selected. It must not read: PostgreSQL guarantees fair queues.
Build a deterministic synthetic queue schema
The schema needs enough state to distinguish locking, ownership, execution attempts, lease recovery, and terminal completion. It does not need production payload complexity. Keep payloads human-readable and job IDs explicit so evidence from concurrent sessions can be compared without reconstructing generated identifiers.
The partial index mirrors the candidate predicate and ordering. It is a hypothesis to inspect, not a promise that PostgreSQL will always choose that index. Planner choices depend on data and statistics, so acceptance evidence records the selected plan rather than asserting one in advance. PostgreSQL’s EXPLAIN documentation describes plans as planner-selected strategies and explicitly notes that selected strategies can vary.
CREATE TABLE queue_jobs (
id bigint PRIMARY KEY,
priority integer NOT NULL DEFAULT 0,
payload text NOT NULL,
state text NOT NULL
CHECK (state IN ('ready', 'claimed', 'done', 'failed')),
available_at timestamptz NOT NULL,
created_at timestamptz NOT NULL,
claimed_by text,
claimed_at timestamptz,
claim_token text,
lease_until timestamptz,
attempts integer NOT NULL DEFAULT 0,
completed_at timestamptz,
last_error text
);
CREATE INDEX queue_jobs_ready_order_idx
ON queue_jobs
(priority DESC, available_at ASC, created_at ASC, id ASC)
WHERE state = 'ready';
CREATE TABLE job_events (
event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
job_id bigint NOT NULL REFERENCES queue_jobs(id),
worker_id text,
attempt integer,
claim_token text,
from_state text,
to_state text NOT NULL,
event_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
note text
);Seed a closed cohort with explicit IDs. Give jobs the same priority where FIFO-like behavior is being tested, vary priority only in tests that intentionally exercise priority ordering, and make one old job the designated contention probe.
INSERT INTO queue_jobs
(id, priority, payload, state, available_at, created_at)
SELECT
n,
0,
'synthetic-job-' || n,
'ready',
TIMESTAMPTZ '2026-09-22 09:00:00+03',
TIMESTAMPTZ '2026-09-22 08:00:00+03' + (n * INTERVAL '1 second')
FROM generate_series(1, 24) AS n;The lab is conceptually adjacent to backend database work; Refonte Learning’s Choosing Your Programming Path describes backend development in terms that include databases, APIs, and server-side business logic.
Fixture | Purpose | Must remain deterministic |
Jobs 1–24 | closed finite cohort | IDs and creation order |
Job 1 | oldest-lock/starvation probe | yes |
Job 2 | slow-execution probe | yes |
worker-a | first claimant/blocker | yes |
worker-b | concurrent claimant | yes |
worker-c | concurrent claimant/recovery | yes |
Ordering columns | candidate rank | immutable after seed |
Payload | synthetic trace label | no external meaning |
Proposed experiment. Recreate this schema before each scenario or restore the same fixture.
Actual observation. Not executed.
Validate the transaction boundary of a claim
A claim should make a short database decision: identify an eligible row, lock it, change durable ownership metadata, return the exact updated row, and commit. PostgreSQL documents that UPDATE … RETURNING returns values computed from rows actually updated. The companion Returning Data from Modified Rows (current PostgreSQL 18 documentation, undated page; accessed September 22, 2026) explains that RETURNING avoids a separate retrieval query for changed rows.
Claim atomically, work outside the transaction
Use an application-generated claim token unique to the attempt. The exact token format is an application design choice; deterministic test tokens such as worker-a-job-7-attempt-1 are sufficient here.
BEGIN;
WITH candidate AS (
SELECT id
FROM queue_jobs
WHERE state = 'ready'
AND available_at <= CURRENT_TIMESTAMP
ORDER BY priority DESC, available_at ASC, created_at ASC, id ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE queue_jobs AS j
SET state = 'claimed',
claimed_by = :worker_id,
claimed_at = CURRENT_TIMESTAMP,
claim_token = :claim_token,
lease_until = CURRENT_TIMESTAMP + :visibility_interval,
attempts = j.attempts + 1
FROM candidate AS c
WHERE j.id = c.id
RETURNING
j.id,
j.payload,
j.claimed_by,
j.claimed_at,
j.claim_token,
j.lease_until,
j.attempts;
COMMIT;Only after COMMIT should the worker begin its synthetic work. PostgreSQL releases row locks at transaction end; keeping execution outside the claim transaction therefore prevents the job’s application runtime from automatically becoming the row-lock lifetime.
The test should prove the opposite case as well. Pause a session between the UPDATE … RETURNING and COMMIT; another claimant should not obtain that same row. Then commit, verify the persisted claimed state, and show that another claimant ignores the row because state = 'ready' no longer matches.
Checkpoint | Expected evidence | Failure meaning |
Before claim | row is ready | fixture problem |
After RETURNING, before commit | owner visible to claimant transaction | update problem |
Concurrent claimant | cannot claim same row | claim isolation failure |
After commit | claim metadata persists | durability/state failure |
During synthetic work | original claim transaction is closed | transaction-boundary failure |
Completion | token and worker must match | fencing failure |
Documented behavior. Row locks are held until transaction end, and RETURNING describes rows actually updated.
Engineering inference. A worker that performs slow application work before committing the claim unnecessarily extends database lock lifetime.
Actual observation. Not executed.
Compare blocking FOR UPDATE with SKIP LOCKED behavior
The baseline experiment needs two sessions and one deliberately locked old row. Session A begins a transaction and locks job 1 without committing. Session B first runs a locking query without SKIP LOCKED, then repeats the scenario with SKIP LOCKED.
PostgreSQL documents the difference directly: an ordinary locking clause waits when the selected row cannot immediately be locked; NOWAIT raises an error instead, while SKIP LOCKED skips rows that cannot immediately be locked. The table-level lock required by the statement is not itself skipped by SKIP LOCKED; PostgreSQL specifies that the skipping behavior applies to row-level locks.
-- Session A
BEGIN;
SELECT id
FROM queue_jobs
WHERE id = 1
FOR UPDATE;
-- Leave Session A open only for the controlled experiment.-- Session B: blocking control
SELECT id
FROM queue_jobs
WHERE state = 'ready'
ORDER BY priority DESC, available_at, created_at, id
FOR UPDATE
LIMIT 1;Reset the fixture, lock job 1 again, then run:-- Session B: skip-locked variant
SELECT id
FROM queue_jobs
WHERE state = 'ready'
ORDER BY priority DESC, available_at, created_at, id
FOR UPDATE SKIP LOCKED
LIMIT 1;What SKIP LOCKED does and does not guarantee
The expected semantic is not “the queue now starts at job 2.” It is: the candidate that cannot immediately be row-locked is skipped, allowing another eligible row to be considered. Which row is next is governed by the query’s ordering and eligibility, not by an undocumented fairness scheduler.
Property | Plain FOR UPDATE | FOR UPDATE SKIP LOCKED |
Locked first candidate | wait | skip it |
Consistent queue-wide snapshot | not implied | explicitly not implied |
Conflicting row obtained twice | no | no |
Strict FIFO through contention | not established | intentionally breakable by skipping |
Equal work per worker | not guaranteed | not guaranteed |
Eventual return to skipped row | separate question | separate question |
Table-lock waiting eliminated | no | no |
Proposed experiment. Record the session start, target job ID, returned job ID, whether Session B waited, and the transaction end that made job 1 lockable again.
Actual observation. Not executed; do not replace the evidence cell with assumed results.
Test ordering under concurrent workers
Start with one worker because concurrency should not be blamed for an ordering bug that is already present in a single-client query. Repeated claims against the closed cohort should follow the total order when no rows are locked and ordering fields remain unchanged.
Then introduce worker-a, worker-b, and worker-c behind a client-side barrier. Give each worker exactly one claim opportunity per round. That synchronization is important: an unrestricted tight loop mostly measures client scheduling and application speed, making it difficult to separate worker scheduling from PostgreSQL claim semantics.
PostgreSQL warns about another ordering edge case: at READ COMMITTED, a locking query with ORDER BY can appear to return rows out of order if it blocks and ordering columns are concurrently changed. The documented workaround of pushing the locking clause into a subquery changes locking breadth and can have performance consequences.
The cleaner queue test avoids that confounder: once a job enters the ready cohort, keep priority, available_at, created_at, and id immutable. State and ownership metadata may change, but ranking data does not.
Test | Concurrency | Deliberate lock? | Acceptance question |
Single-worker sequence | 1 | no | does the total order hold? |
Barrier round | 3 | no | do workers receive distinct jobs? |
Barrier round with job 1 locked | 3 | yes | is locked job bypassed without duplicate claims? |
Lock released before next round | 3 | no | does job 1 regain its proper candidate rank? |
Immutable-order test | 3 | no | are order fields unchanged throughout? |
For every claim, capture the queue snapshot’s expected rank immediately before the synchronized round where practical, plus the returned job ID. Do not interpret different numbers of total jobs per worker as a PostgreSQL ordering violation; a faster client can simply issue more claims when the harness is not synchronized.
Engineering inference. Equal job counts per worker are a poor acceptance criterion for SELECT FOR UPDATE queue fairness because the database does not promise round-robin worker scheduling.
Actual observation. Not executed.
Test starvation of older or repeatedly skipped jobs
Starvation is the most important property that “we never saw a duplicate” fails to address. A row can be correctly excluded from every claim while locked. The relevant operational question is what happens after the condition causing it to be skipped disappears.
Create the strongest deterministic probe: job 1 is the oldest job and highest-ranked candidate under the total order. Hold its row lock in Session A. Let the other workers claim repeated batches with SKIP LOCKED. The expected test evidence should show job 1 remaining ready but inaccessible while its lock exists, while newer rows make progress.
PostgreSQL’s documentation is enough to explain the skipping, but it does not promise starvation freedom. In a separate PostgreSQL UPDATE batching example, the documentation even notes that SKIP LOCKED can help prevent concurrent commands from selecting the same rows while a final pass is still needed to ensure rows are not overlooked. That is strong guidance against equating “skip” with “eventually serviced.” PostgreSQL 18 UPDATE documentation (current documentation, undated page; accessed September 22, 2026).
Detecting an old job that repeatedly loses access
Capture a skip-probe snapshot after every batch:
SELECT
id,
state,
priority,
available_at,
created_at,
attempts,
claimed_by,
claimed_at
FROM queue_jobs
WHERE state = 'ready'
ORDER BY priority DESC, available_at, created_at, id;After several batches, release Session A’s lock. Do not insert a more-preferred job between release and the next synchronized claim. Under the lab’s defined fairness invariant, job 1 should then be the first eligible unlocked candidate.
Phase | Job 1 condition | Expected interpretation |
Initial | oldest, ready, unlocked | should rank first |
Blocked | oldest, ready, row locked | may be skipped |
Repeated batches | still locked | skipping is not yet starvation failure |
Lock released | ready, unlocked | must become accessible again |
Next controlled claim | highest test rank | should be claimed |
Never reclaimed | unlocked but continuously bypassed | reject and investigate |
A second starvation test should keep queue depth high. Maintain a backlog larger than the synchronized claim batch, but mark the original cohort separately from newly inserted jobs. Acceptance should focus on whether each original, continuously eligible job eventually progresses, rather than whether total queue depth reaches zero.
Engineering inference. Queue depth can stay high while an old cohort still makes acceptable progress. Conversely, aggregate completions can look healthy while one old job remains stranded. Therefore, progress evidence needs per-job age and identity, not only aggregate counters.
Actual observation. Not executed; a small synthetic pass would not prove production starvation freedom even if successful.
Test long-running work without holding unnecessary locks
A deliberately slow job is useful only if it tests the right transaction boundary. Claim job 2, commit, then simulate slow work with a local application sleep. Do not hold the claim transaction open while sleeping.
PostgreSQL states that row-level locks are released at transaction end and recommends avoiding long-lived transactions. A row-level FOR UPDATE lock prevents competing writers and lockers on that row while the transaction remains open.
The important distinction is between job ownership and database lock ownership. After the claim commits, the application’s durable state='claimed', claimed_by, token, and lease represent ownership. A row lock does not need to survive for the entire business task if the claim predicate excludes committed claimed jobs.
This separation is also relevant to ordinary backend design, where database transactions should match the atomic database decision rather than the duration of arbitrary application work. Refonte Learning’s API Developer coverage connects API/backend work with database integration and server-side execution boundaries.
Variant | Claim commit | Synthetic delay | Row lock during delay | Accept? |
A | before work | local sleep | no claim lock retained | yes |
B | after work | local sleep inside transaction | retained | reject design |
C | before work | no delay | no unnecessary lock | control |
D | rollback before commit | no work should start | claim disappears | control |
Run both A and B only in the disposable database so the difference is visible. While A sleeps, other workers should continue claiming other ready jobs. While B holds its transaction, the locked row becomes another skip target and the open transaction is intentionally demonstrating the anti-pattern.
Engineering inference. A transaction that spans arbitrary application work expands lock lifetime and failure scope without being required by SKIP LOCKED.
Actual observation. Not executed; record transaction IDs or session identifiers, claim timestamps, commit timestamps, and worker work-start/work-end timestamps when the lab is run.
Test abandoned claims and visibility recovery
There are two materially different “worker died” moments.
If a worker disappears before its claim transaction commits, PostgreSQL transaction rollback semantics remove its uncommitted state and transaction-held locks cease when the transaction ends. If the application connection truly terminates, another worker can later compete for the row again. Row locks themselves are not durable lease records.
If a worker disappears after committing state='claimed', the database has exactly what the application asked it to persist: a claimed row. SKIP LOCKED will not magically return that row to ready, because the problem is no longer an uncommitted row lock. Recovery now belongs to the queue’s application-level visibility policy.
Visibility timeout versus transaction lifetime
Treat lease_until as an explicit application invariant. A recovery process can make an expired synthetic claim eligible again:
UPDATE queue_jobs
SET state = 'ready',
claimed_by = NULL,
claimed_at = NULL,
claim_token = NULL,
lease_until = NULL,
available_at = CURRENT_TIMESTAMP,
last_error = 'lease expired; synthetic recovery'
WHERE state = 'claimed'
AND lease_until < CURRENT_TIMESTAMP
RETURNING id, attempts;That recovery statement is not a PostgreSQL SKIP LOCKED feature. It is queue policy layered on ordinary transactional updates.
Failure moment | Durable claimed state? | Row lock survives failed transaction? | Recovery mechanism |
Before claim update | no | no | ordinary future claim |
After update, before commit | no after rollback | no after transaction ends | ordinary future claim |
Immediately after commit | yes | claim lock already released | visibility/lease policy |
During slow work | yes | no claim lock required | wait for worker or lease |
After lease expiry | yes until reaper acts | no | recovery update |
Stale worker returns later | possibly | no | token fencing |
The proposed abandoned-claim experiment should claim one job with worker-a, commit, and then intentionally stop that worker before completion. Wait only as required by the configured synthetic visibility interval, run recovery, and let worker-b claim the row with a new token.
Then restart the old completion path using worker-a’s token. That completion must fail its ownership predicate.
Engineering inference. A visibility timeout is a durable ownership-recovery rule; a PostgreSQL transaction lifetime is a database concurrency boundary. Conflating them leads either to unnecessarily long transactions or permanently abandoned claimed rows.
Actual observation. Not executed.
Test retries, duplicate execution, and idempotency boundaries
A queue can have perfectly serialized claims and still execute logical work twice. Consider worker-a: it commits a claim, performs the synthetic business operation, and fails before recording completion. The lease later expires. worker-b reclaims the job because the database has no proof that the earlier business operation finished.
Nothing about SKIP LOCKED makes that ambiguity disappear. The database can serialize access to queue rows; it cannot infer whether an operation performed outside the claim transaction happened before a process died.
Duplicate execution as an application invariant
Model that problem inside the lab without external effects. Add a synthetic effects table whose primary key represents one logical effect:
CREATE TABLE synthetic_effects (
job_id bigint PRIMARY KEY REFERENCES queue_jobs(id),
first_worker text NOT NULL,
first_attempt integer NOT NULL,
recorded_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);On the first execution, insert the effect. Simulate a worker crash before marking the queue job done. After visibility recovery, let a different worker execute the same job and attempt the same insert. The uniqueness rule should expose the duplicate attempt rather than silently manufacturing a second synthetic effect.
Completion must also be fenced:
UPDATE queue_jobs
SET state = 'done',
completed_at = CURRENT_TIMESTAMP,
lease_until = NULL
WHERE id = :job_id
AND state = 'claimed'
AND claimed_by = :worker_id
AND claim_token = :claim_token
RETURNING id, attempts, completed_at;Because PostgreSQL UPDATE … RETURNING reports the rows actually updated, an empty result gives the application a clean way to detect that its ownership predicate no longer matches.
Failure injection | Expected attempt history | Acceptance property |
Fail before claim commit | no durable attempt ownership | another worker may claim |
Fail after claim commit, before effect | retry later | one eventual effect |
Fail after effect, before completion | duplicate effect attempt possible | duplicate detected/idempotently contained |
Lease expires during slow worker | second claim possible | stale token fenced |
Old worker completes after reclaim | old token mismatches | update affects no row |
Ordinary application error | retry policy decides next eligibility | attempts auditable |
Engineering inference. Exactly-once business execution is not established by FOR UPDATE SKIP LOCKED; it must be enforced or made harmless at the application/effect boundary.
Actual observation. Not executed.
Observe queue/index behavior without inventing production benchmarks
Acceptance should inspect query behavior, but a disposable synthetic database cannot justify production capacity claims. Do not publish requests per second, production latency, CPU sizing, worker-count recommendations, or claims about how many rows this design can process based on this lab.
Use EXPLAIN first because it plans without executing the statement. PostgreSQL documents that EXPLAIN shows how tables will be scanned and which plan the planner chose. EXPLAIN ANALYZE, by contrast, actually executes the supplied statement; PostgreSQL warns that data-changing side effects therefore occur unless they are contained and rolled back. PostgreSQL 18 EXPLAIN documentation (current documentation, undated page; accessed September 22, 2026).
EXPLAIN (VERBOSE, COSTS)
SELECT id
FROM queue_jobs
WHERE state = 'ready'
AND available_at <= CURRENT_TIMESTAMP
ORDER BY priority DESC, available_at, created_at, id
LIMIT 1;If the team wants actual buffers and row counts, use EXPLAIN (ANALYZE, BUFFERS) only in the disposable fixture and remember that it executes the query. For a mutating claim statement, wrap exploratory execution in BEGIN/ROLLBACK exactly as the PostgreSQL documentation recommends for side-effecting statements under EXPLAIN ANALYZE.
Refonte Learning’s Database Administrator in 2026: Skills, Salary & Projects provides the broader cloud-database and operational context for plan reading, monitoring, backup/recovery, security, and performance work.
Evidence | Record | Do not infer |
Query plan | scan type, index name, sort node | universal plan stability |
Actual lab buffers | hits/reads if deliberately measured | production I/O |
Row counts | fixture counts | production cardinality |
Lock behavior | blocked/skipped job IDs | capacity |
Queue depth | synthetic ready/claimed/done counts | real workload SLA |
Worker history | claim distribution | PostgreSQL scheduler fairness |
Planner cost | relative planner estimate | wall-clock latency |
Also inspect whether ordering requires an explicit sort and whether the partial index is considered. A sequential scan on a tiny fixture is not automatically a defect: the planner is free to choose the plan it estimates cheapest. PostgreSQL explicitly describes planner cost as an estimate rather than measured execution time.
Actual observation. Not executed; paste the real plans into the evidence package rather than writing expected plan names as results.
Define fairness and progress acceptance thresholds
The acceptance criteria should be strict where correctness is deterministic and deliberately modest where PostgreSQL provides no fairness guarantee.
For duplicate live claims, the threshold can be absolute: zero accepted instances of two current ownership tokens for the same job attempt. For stale completions, zero old-token completions are acceptable. For a finite cohort after blockers and recovery conditions have been cleared, every job must eventually reach the defined terminal disposition.
For ordering, the threshold is conditional. When the highest-ranked candidate is eligible and unlocked, it should win the next controlled claim in the deterministic harness. While it is locked, newer rows may pass it because that is precisely what SKIP LOCKED requests.
Progress evidence across multiple workers
Do not demand a 33/33/33 split among three workers. Instead, use barrier-controlled rounds in which each worker is permitted one claim while at least that many eligible rows exist. The purpose is to prove distinct claims and pool progress, not an undocumented round-robin scheduler.
For the high-depth test, tag the original cohort logically and keep adding synthetic ready rows so total depth does not drain. The cohort passes if its members continue transitioning rather than becoming permanently hidden behind newer work.
Acceptance dimension | Pass threshold | Why |
Concurrent live claim collision | zero | hard correctness invariant |
Stale-token completion | zero successful stale completions | ownership fencing invariant |
Closed finite cohort | all jobs reach accepted terminal disposition | operational starvation test |
Released oldest probe | claimed on next controlled opportunity when still highest-ranked | defined fairness invariant |
Temporary locked probe | may be skipped | documented SKIP LOCKED behavior |
Attempts | every reclaim increments/audits attempt | retry traceability |
High-depth original cohort | every continuously eligible member progresses | detects hidden starvation |
Worker distribution | record only; no equality threshold | no PostgreSQL fairness guarantee |
The READ COMMITTED default also matters when interpreting snapshots. PostgreSQL states that successive commands in one transaction can see different committed data when concurrent transactions commit between those commands. Updating/locking commands may also re-evaluate rows after waiting on concurrent changes. PostgreSQL 18 Transaction Isolation (current documentation, undated page; accessed September 22, 2026).
That is another reason to record timestamps and state transitions rather than trying to reconstruct concurrency from a single final table dump.
Engineering inference. Passing these tests demonstrates consistency with the playbook’s operational fairness definition under its synthetic conditions. It does not prove universal or production-scale PostgreSQL queue fairness.
Actual observation. Not executed.
Build an evidence package and failure classification
A useful acceptance run should be reproducible by someone who did not run it. Screenshots alone are insufficient because they often omit transaction boundaries, exact SQL, or event chronology. Preserve the schema, seed SQL, worker commands, captured rows, and experiment-specific logs together.
At minimum, each successful claim record needs the job ID, worker ID, claim timestamp, token, attempt, previous state, resulting state, and transaction completion point. Each job completion needs its completion timestamp. Recovery needs the expired lease and the worker that subsequently reclaimed the job.
The final state dump is necessary but not sufficient. Two different histories can produce the same final done row. The event log is what distinguishes “claimed once and completed” from “claimed, expired, reclaimed, stale worker returned, second worker completed.”
Classify failures by violated invariant rather than by vague labels such as “race condition.”
Failure class | Evidence pattern | Disposition |
Claim collision | same job/attempt concurrently assigned twice | reject |
Ordering defect | unlocked highest-ranked job bypassed in controlled fixture | reject/investigate SQL |
Expected skip | deliberately locked candidate bypassed | expected |
Starvation defect | unlocked eligible probe remains bypassed after blocker removal | reject |
Long-transaction defect | work sleep occurs before claim commit | reject design |
Recovery defect | expired committed claim never becomes eligible | reject |
Fencing defect | stale token completes newer attempt | reject |
Duplicate-effect exposure | retry repeats logical effect | require idempotency control |
Unequal worker totals | workers claim different totals | informational unless harness itself requires equal opportunities |
Plan variation | planner selects another valid plan | investigate only if acceptance objective is violated |
The evidence directory can be as simple as:
evidence/
schema.sql
seed.sql
claim.sql
complete.sql
recover.sql
experiment-blocking.txt
experiment-ordering.csv
experiment-starvation.csv
experiment-abandoned-claim.csv
experiment-duplicate-effect.csv
query-plans.txt
final-jobs.csv
job-events.csv
acceptance.mdA per-experiment row should state documented basis, engineering hypothesis, procedure, expected acceptance condition, and actual observation. Until somebody runs the lab, the last field remains N,cO39T EXECUTED.
This style of evidence aligns with database-administration practice because it makes recovery and performance conclusions auditable rather than anecdotal. Refonte Learning’s database content also emphasizes monitoring, operational health, performance, security, and recovery as distinct parts of database work.
Actual observation. No evidence artifacts were generated for this article because executing tests would violate the requested publication boundary.
Final acceptance decision and Refonte Learning CTA
Decision gate | Accept only when |
Claim correctness | concurrent workers obtain distinct live claims |
Transaction boundary | claim commits before synthetic work begins |
Skip semantics | blocked rows are distinguished from starved rows |
Ordering | controlled unlocked candidates obey the declared total order |
Starvation | released probes and the finite cohort eventually progress |
Abandoned claims | committed expired ownership is recoverable |
Retries | attempts are auditable and stale tokens are fenced |
Duplicate effects | duplicate execution is explicitly detected or made idempotent |
Evidence | job IDs, worker IDs, timestamps, attempts, transitions, plans, and failures are preserved |
Scope | conclusions remain synthetic-lab conclusions, not production fairness claims |
Verified program context | 3 months; 12–14 hours/week; database design/architecture, SQL optimization, backup/recovery, performance tuning, security, cloud database management, migration/integration, disaster recovery, RBAC, monitoring, and maintenance |
Use this acceptance gate to sign off a SKIP LOCKED worker design only after every required correctness artifact is green.
Build the broader operational skill set through Refonte Learning’s Database Administrator Essentials program.
