A database table is a queue until SELECT FOR UPDATE says otherwise

A jobs table is a perfectly good queue for one worker. Add a second and both claim the same row, because SELECT then UPDATE is two statements with a gap in between.

START TRANSACTION;

SELECT id FROM jobs
 WHERE status = 'pending'
 ORDER BY id
 LIMIT 1
 FOR UPDATE;          -- other workers block here

UPDATE jobs SET status = 'running' WHERE id = ?;

COMMIT;

FOR UPDATE locks the selected rows until the transaction ends, so the second worker waits rather than duplicating. The cost is that it genuinely waits — with many workers the queue serialises on that lock, which is the point at which a real broker starts to look reasonable. Keep the transaction short: claim the row, commit, then do the work outside it.