[{"data":1,"prerenderedAt":17},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:en":3,"api:\u002Fapi\u002Fnotes\u002Flease-and-fencing:en":8},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","en","Ewsirdev","Web developer: websites, landing pages and Go services",{"slug":9,"lang":5,"title":10,"date":11,"summary":12,"project":13,"minutes":14,"body_md":15,"body_html":16},"lease-and-fencing","Lease and fencing: how a queue neither loses a job nor lets a zombie finish it twice","2026-10-20","A worker can crash, stall, or wake up after its job was taken by someone else. How the lease and the lease token in job-queue deal with that on SQLite.","job-queue",2,"A background queue has two nasty situations. A worker dies in the middle of a job and the job stays \"running\" forever. Or a worker simply freezes for a while (a GC pause, the network), the job is handed to another worker, and then the first one wakes up and writes its result over someone else's.\n\n## Lease\n\nClaiming a job grants a **lease**: a `lease_until` deadline (30 seconds by default). While the handler runs, the worker extends the lease with a heartbeat every third of the term. If the process dies, the heartbeat stops, the lease expires, and the \"reaper\" returns the job to the queue with backoff or, if attempts are exhausted, to dead.\n\n## Claiming with one statement\n\nPicking the job and granting the lease is a single SQL statement:\n\n```sql\nUPDATE jobs SET status='running', attempts=attempts+1,\n  lease_until=?, worker=?, token=?, started_at=?\nWHERE id=(SELECT id FROM jobs WHERE status='pending' AND run_at\u003C=?\n          ORDER BY priority DESC, run_at ASC, id ASC LIMIT 1)\nRETURNING ...\n```\n\nSo two workers, even from different processes on one database file, cannot get the same job.\n\n## Fencing\n\nEvery claim stores a unique token. Extending, completing and failing run with `WHERE status='running' AND token=?`. If the job was already taken over, the condition does not match, and the zombie worker gets `ErrLeaseLost` instead of corrupting the result.\n\n## What it does not give you\n\nThe guarantee stays **at-least-once**: after a lease expires, a handler may be called again, so it must be idempotent. The queue does not promise exactly-once. Details and limitations are in the project README.\n","\u003Cp>A background queue has two nasty situations. A worker dies in the middle of a job and the job stays &#34;running&#34; forever. Or a worker simply freezes for a while (a GC pause, the network), the job is handed to another worker, and then the first one wakes up and writes its result over someone else&#39;s.\u003C\u002Fp>\n\u003Ch2>Lease\u003C\u002Fh2>\n\u003Cp>Claiming a job grants a \u003Cstrong>lease\u003C\u002Fstrong>: a \u003Ccode>lease_until\u003C\u002Fcode> deadline (30 seconds by default). While the handler runs, the worker extends the lease with a heartbeat every third of the term. If the process dies, the heartbeat stops, the lease expires, and the &#34;reaper&#34; returns the job to the queue with backoff or, if attempts are exhausted, to dead.\u003C\u002Fp>\n\u003Ch2>Claiming with one statement\u003C\u002Fh2>\n\u003Cp>Picking the job and granting the lease is a single SQL statement:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\">\u003Ccode class=\"lang-sql\">UPDATE jobs SET status=&#39;running&#39;, attempts=attempts+1,\n  lease_until=?, worker=?, token=?, started_at=?\nWHERE id=(SELECT id FROM jobs WHERE status=&#39;pending&#39; AND run_at&lt;=?\n          ORDER BY priority DESC, run_at ASC, id ASC LIMIT 1)\nRETURNING ...\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>So two workers, even from different processes on one database file, cannot get the same job.\u003C\u002Fp>\n\u003Ch2>Fencing\u003C\u002Fh2>\n\u003Cp>Every claim stores a unique token. Extending, completing and failing run with \u003Ccode>WHERE status=&#39;running&#39; AND token=?\u003C\u002Fcode>. If the job was already taken over, the condition does not match, and the zombie worker gets \u003Ccode>ErrLeaseLost\u003C\u002Fcode> instead of corrupting the result.\u003C\u002Fp>\n\u003Ch2>What it does not give you\u003C\u002Fh2>\n\u003Cp>The guarantee stays \u003Cstrong>at-least-once\u003C\u002Fstrong>: after a lease expires, a handler may be called again, so it must be idempotent. The queue does not promise exactly-once. Details and limitations are in the project README.\u003C\u002Fp>\n",1792697892653]