{
  "id": 17943335,
  "title": "Two Pods, One Job Table: A Safe Postgres Queue with SKIP LOCKED in Spring Boot",
  "url": "https://urgent.news/2026/11/03/two-pods-one-job-table-a-safe-postgres-queue-with-skip-locked-in",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-11-03T09:56:30.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/shreyash_kakde/two-pods-one-job-table-a-safe-postgres-queue-with-skip-locked-in-spring-boot-4l79"
  },
  "original_language": "en",
  "account": "On a Monday morning, a support representative sent the same \"Your invoice is ready\" email to a customer three times. The system's dashboards showed normal performance, with low CPU usage and zero error rate. However, the logs revealed that the NotificationWorker was picking the same 50 jobs three times from the notification_job table, despite there being only one replica of the NotificationJob table.\n\nThe issue stemmed from the use of plain SELECT queries to fetch jobs without specifying the lock mode. PostgreSQL's default READ COMMITTED isolation level allows multiple pods to read the same set of pending jobs without locking them. As a result, all three pods ended up processing the same 50 jobs, leading to duplicate emails being sent.\n\nTo address this, the article suggests using a FOR UPDATE SKIP LOCKED clause in combination with a short transaction. This approach allows the pods to claim a set of jobs in a short transaction, process them, and then update the status of the processed jobs as DONE in a separate statement. By using SKIP LOCKED, the pods can avoid lock contention and process jobs in parallel without worrying about locking the same rows.\n\nThe article also provides a code example that demonstrates how to implement this pattern using Spring Data JPA. By setting the status column to RUNNING in the same transaction as selecting the jobs, the pods can ensure that each job is processed only once, even when running in multiple JVMs simultaneously. The key points to remember are:\n\n1. Use FOR UPDATE SKIP LOCKED in a short transaction to claim a set of jobs without locking them permanently.\n2. Perform the status update (e.g., setting status to DONE) in a separate statement after processing the jobs.\n3. Keep the transaction duration short to release the row locks and allow other pods to process the remaining jobs.\n\nBy following this approach, the NotificationWorker can efficiently handle the job queue using a PostgreSQL table without the need for an external message broker.",
  "summary": "It's 10:20 AM on a Monday and support has forwarded three screenshots of the same \"Your invoice is ready\" email, received three times by one customer. The dashboards look fine. CPU is low, the error rate is zero. Your logs show this: 10:15:00.112 [scheduling-1] INFO c.a.n.NotificationWorker - picked 50 jobs (pod notif-7c9d-a) 10:15:00.115 [scheduling-1] INFO c.a.n.NotificationWorker - picked 50…",
  "key_points": [
    "NotificationWorker processed same jobs three times due to lack of locking mechanism",
    "FOR UPDATE SKIP LOCKED clause prevents lock contention in PostgreSQL",
    "Spring Boot code example demonstrates efficient job queue processing"
  ],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}