Medium-High Impact

Use SKIP LOCKED for Non-Blocking Queue Processing

Process queue items concurrently without blocking

Why This Matters

FOR UPDATE SKIP LOCKED allows multiple workers to process queue items concurrently without blocking each other. When a row is locked by one worker, other workers simply skip it and pick the next available item.

This is perfect for:

Use case: With 10 workers processing a job queue, each worker can grab a different job simultaneously without waiting for others to finish.

The Problem

Without SKIP LOCKED, workers block waiting for locks, reducing parallelism:

Incorrect - Workers block waiting for locks
-- Worker 1: Gets job 1, locks it
BEGIN;
SELECT * FROM jobs 
WHERE status = 'pending' 
ORDER BY created_at LIMIT 1
FOR UPDATE;
-- Processing job 1...

-- Worker 2: Waits for Worker 1's lock to release
BEGIN;
SELECT * FROM jobs 
WHERE status = 'pending' 
ORDER BY created_at LIMIT 1
FOR UPDATE;
-- BLOCKED! Waiting for Worker 1...

-- Worker 3: Also blocked waiting...
-- Only one worker processes at a time!

Workers wait in line instead of processing different jobs simultaneously, wasting resources and slowing down queue processing.

The Solution

Use FOR UPDATE SKIP LOCKED to skip locked rows and pick the next available item:

Correct - Workers process different jobs concurrently
-- Worker 1: Gets job 1, locks it
BEGIN;
SELECT * FROM jobs 
WHERE status = 'pending' 
ORDER BY created_at LIMIT 1
FOR UPDATE SKIP LOCKED;
-- Returns job 1, processing...

-- Worker 2: Skips locked job 1, gets job 2
BEGIN;
SELECT * FROM jobs 
WHERE status = 'pending' 
ORDER BY created_at LIMIT 1
FOR UPDATE SKIP LOCKED;
-- Returns job 2 immediately! Processing concurrently...

-- Worker 3: Gets job 3, also processing concurrently
-- All workers process different jobs simultaneously!

Complete queue worker pattern:

-- Queue table
CREATE TABLE jobs (
  id BIGSERIAL PRIMARY KEY,
  status TEXT NOT NULL DEFAULT 'pending',
  payload JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CHECK (status IN ('pending', 'processing', 'completed', 'failed'))
);

CREATE INDEX jobs_status_created_idx ON jobs (status, created_at)
WHERE status = 'pending';

-- Worker function
CREATE OR REPLACE FUNCTION process_next_job()
RETURNS TABLE(job_id BIGINT, job_payload JSONB) AS $$
DECLARE
  job_record jobs%ROWTYPE;
BEGIN
  -- Get and lock next available job
  SELECT * INTO job_record
  FROM jobs
  WHERE status = 'pending'
  ORDER BY created_at
  LIMIT 1
  FOR UPDATE SKIP LOCKED;
  
  IF job_record.id IS NULL THEN
    -- No jobs available
    RETURN;
  END IF;
  
  -- Mark as processing
  UPDATE jobs SET status = 'processing' WHERE id = job_record.id;
  
  -- Return job details
  RETURN QUERY SELECT job_record.id, job_record.payload;
END;
$$ LANGUAGE plpgsql;

Result: Multiple workers can process jobs simultaneously without blocking. With 10 workers, you get 10x better throughput compared to sequential processing.

Best Practices

Warning: SKIP LOCKED doesn't guarantee fairness. Workers might process newer jobs before older ones if locks are held. Use ORDER BY to maintain priority.

Example: Multi-Worker Email Queue

Complete example with multiple workers processing emails:

-- Email queue table
CREATE TABLE email_queue (
  id BIGSERIAL PRIMARY KEY,
  to_email TEXT NOT NULL,
  subject TEXT NOT NULL,
  body TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  priority INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Index for efficient queue processing
CREATE INDEX email_queue_pending_idx ON email_queue (priority DESC, created_at)
WHERE status = 'pending';

-- Worker: Get next email to send
BEGIN;
SELECT * FROM email_queue
WHERE status = 'pending'
ORDER BY priority DESC, created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;

-- If row found, mark as processing and send
UPDATE email_queue SET status = 'processing' WHERE id = -- job id;
COMMIT;

-- Send email (outside transaction!)
-- send_email(to_email, subject, body)

-- Mark as completed
UPDATE email_queue SET status = 'completed' WHERE id = -- job id;

Check Your Project Automatically

Connect your Supabase project to detect queue processing issues and optimize concurrency patterns.

Start Supabase Audit

Learn More