<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Om singh Chouhan</title>
    <description>The latest articles on DEV Community by Om singh Chouhan (@om_singhchouhan_7a318ed6).</description>
    <link>https://dev.to/om_singhchouhan_7a318ed6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4224799%2F34fba8f9-9b6d-482c-9bb6-1b29eaf79639.png</url>
      <title>DEV Community: Om singh Chouhan</title>
      <link>https://dev.to/om_singhchouhan_7a318ed6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/om_singhchouhan_7a318ed6"/>
    <language>en</language>
    <item>
      <title>How I solved the "crashed worker" problem in Go (Lease Fencing)</title>
      <dc:creator>Om singh Chouhan</dc:creator>
      <pubDate>Tue, 03 Nov 2026 18:02:50 +0000</pubDate>
      <link>https://dev.to/om_singhchouhan_7a318ed6/how-i-solved-the-crashed-worker-problem-in-go-lease-fencing-2ka4</link>
      <guid>https://dev.to/om_singhchouhan_7a318ed6/how-i-solved-the-crashed-worker-problem-in-go-lease-fencing-2ka4</guid>
      <description>&lt;h1&gt;
  
  
  Handling Stale Workers in Go: Why I Chose Lease Fencing Over Distributed Locks
&lt;/h1&gt;

&lt;p&gt;When building background workers that process tasks—sending webhooks, processing images, or syncing data—the hardest problem isn't the happy path.&lt;/p&gt;

&lt;p&gt;It's this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when a worker crashes in the middle of a task?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been building &lt;a href="https://github.com/rajputomsingh/Relay" rel="noopener noreferrer"&gt;Relay&lt;/a&gt;, an open-source webhook delivery service written in Go and backed by MongoDB.&lt;/p&gt;

&lt;p&gt;A webhook delivery is essentially a background job: we send an HTTP request to a destination, wait for a response, and persist the result.&lt;/p&gt;

&lt;p&gt;The naive approach to worker crashes is simple: retry the job.&lt;/p&gt;

&lt;p&gt;But what if a worker dies while the HTTP request is still in flight?&lt;/p&gt;

&lt;p&gt;You're left with some difficult distributed systems questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the request reach the downstream server?&lt;/li&gt;
&lt;li&gt;Did the downstream server actually process it?&lt;/li&gt;
&lt;li&gt;If we retry, could we trigger the same side effect twice—for example, charging a credit card twice?&lt;/li&gt;
&lt;li&gt;If we don't retry, could the event be lost permanently?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no reliable way for the delivery engine to know exactly what happened after a connection is interrupted.&lt;/p&gt;

&lt;p&gt;For Relay, I chose &lt;strong&gt;at-least-once delivery&lt;/strong&gt;. This means a delivery can happen more than once, so downstream consumers should use idempotency keys to protect against duplicate side effects.&lt;/p&gt;

&lt;p&gt;But that decision introduced another problem I needed to solve: &lt;strong&gt;stale workers&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stale Worker Problem
&lt;/h2&gt;

&lt;p&gt;Imagine the following sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Worker A&lt;/strong&gt; claims a delivery job and receives a lease valid for 30 seconds.&lt;/li&gt;
&lt;li&gt;Worker A sends the HTTP request to the downstream server.&lt;/li&gt;
&lt;li&gt;The downstream server processes the request successfully.&lt;/li&gt;
&lt;li&gt;Before Worker A can persist the successful result, its process experiences a long pause—perhaps due to a runtime pause, VM suspension, or network issue.&lt;/li&gt;
&lt;li&gt;The 30-second lease expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker B&lt;/strong&gt; detects the expired lease, claims the job, and sends the HTTP request again.&lt;/li&gt;
&lt;li&gt;Worker A eventually resumes and attempts to write its result to the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now imagine Worker A writes an outdated result after Worker B has already reclaimed the job.&lt;/p&gt;

&lt;p&gt;Without additional protection, Worker A could overwrite newer delivery state.&lt;/p&gt;

&lt;p&gt;This is the stale worker problem: &lt;strong&gt;a worker can continue executing even after it has lost ownership of the job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lease expiration doesn't physically stop a worker. It only means the system is allowed to give the job to someone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Lease Fencing
&lt;/h2&gt;

&lt;p&gt;To solve this, I used a technique called &lt;strong&gt;lease fencing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of treating a lease as a simple boolean lock, the database assigns an incrementing integer called a &lt;em&gt;fencing token&lt;/em&gt; every time a job is claimed.&lt;/p&gt;

&lt;p&gt;The token represents the ownership generation of that job.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker A claims the job. The database assigns &lt;code&gt;lease_token = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Worker A stores token &lt;code&gt;1&lt;/code&gt; in memory.&lt;/li&gt;
&lt;li&gt;Its lease expires.&lt;/li&gt;
&lt;li&gt;Worker B claims the same job. The database atomically increments the token to &lt;code&gt;2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Worker B now owns the latest lease generation.&lt;/li&gt;
&lt;li&gt;Worker A wakes up and tries to persist its result using token &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The database rejects the update because the stored token is now &lt;code&gt;2&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worker A has been fenced out.&lt;/p&gt;

&lt;p&gt;Its result cannot overwrite the state written by the newer owner.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this looks like in Go
&lt;/h3&gt;

&lt;p&gt;Using the MongoDB Go driver, a worker can include its fencing token in every state-changing update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;deliveryID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"lease_token"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"$set"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update delivery: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MatchedCount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// We no longer own this delivery.&lt;/span&gt;
    &lt;span class="c"&gt;// Another worker has reclaimed it.&lt;/span&gt;
    &lt;span class="c"&gt;// Drop the stale result.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part isn't the update itself.&lt;/p&gt;

&lt;p&gt;It's the condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="s"&gt;"lease_token"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myToken&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database only accepts the update if the worker's token still matches the current token.&lt;/p&gt;

&lt;p&gt;This comparison and update happen atomically as part of the database operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule is simple: a worker can only modify delivery state while its fencing token is still current.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same ownership check must apply to every state-changing operation—not just successful delivery updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use Distributed Locks Like Redis?
&lt;/h2&gt;

&lt;p&gt;Distributed locks are useful, but a lock with a time-to-live (TTL) doesn't automatically solve the stale worker problem.&lt;/p&gt;

&lt;p&gt;Consider this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker A acquires a Redis lock.&lt;/li&gt;
&lt;li&gt;Worker A pauses longer than the lock TTL.&lt;/li&gt;
&lt;li&gt;The lock expires.&lt;/li&gt;
&lt;li&gt;Worker B acquires the lock.&lt;/li&gt;
&lt;li&gt;Worker A resumes and continues executing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both workers may now believe they can perform work.&lt;/p&gt;

&lt;p&gt;A lock tells a worker whether it acquired ownership at some point. It doesn't necessarily prevent an old owner from writing stale state after ownership changes.&lt;/p&gt;

&lt;p&gt;Lease fencing addresses this at the persistence layer.&lt;/p&gt;

&lt;p&gt;The database checks the fencing token when accepting a state change, rather than trusting that the worker still owns the lease.&lt;/p&gt;

&lt;p&gt;That is the important distinction: &lt;strong&gt;ownership must be enforced where state is written.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-off
&lt;/h2&gt;

&lt;p&gt;There is an interesting trade-off here.&lt;/p&gt;

&lt;p&gt;Suppose Worker A successfully delivers a webhook, but its lease expires before it can save the success result.&lt;/p&gt;

&lt;p&gt;Worker B reclaims the delivery and sends the webhook again.&lt;/p&gt;

&lt;p&gt;Worker A eventually resumes, but its completion update is rejected because its fencing token is stale.&lt;/p&gt;

&lt;p&gt;The system may therefore lose Worker A's success signal and perform another delivery attempt.&lt;/p&gt;

&lt;p&gt;That is intentional.&lt;/p&gt;

&lt;p&gt;With at-least-once delivery, duplicate attempts are possible. Downstream systems should use stable event IDs or idempotency keys to prevent duplicate side effects.&lt;/p&gt;

&lt;p&gt;I would rather retry a webhook than allow an outdated worker to corrupt the delivery engine's state machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery duplication is an accepted trade-off. State corruption is not.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building This Into Relay
&lt;/h2&gt;

&lt;p&gt;I've been building &lt;a href="https://github.com/rajputomsingh/Relay" rel="noopener noreferrer"&gt;Relay&lt;/a&gt; to explore these infrastructure problems in a reusable, open-source project.&lt;/p&gt;

&lt;p&gt;The goal is to provide a reliable webhook delivery engine without having to rebuild the same background-processing infrastructure for every application.&lt;/p&gt;

&lt;p&gt;Some of the areas I'm working on include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bounded concurrent workers&lt;/li&gt;
&lt;li&gt;At-least-once webhook delivery&lt;/li&gt;
&lt;li&gt;Lease-based job ownership&lt;/li&gt;
&lt;li&gt;Fencing tokens to reject stale worker updates&lt;/li&gt;
&lt;li&gt;Exponential backoff and automatic retries&lt;/li&gt;
&lt;li&gt;Delivery state management and observability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm also building out the test coverage and benchmarking harness to understand how the system behaves under concurrency and failure conditions.&lt;/p&gt;

&lt;p&gt;If you're interested in contributing, I've curated a list of &lt;a href="https://github.com/rajputomsingh/Relay/issues/81" rel="noopener noreferrer"&gt;&lt;code&gt;good first issues&lt;/code&gt;&lt;/a&gt;, including documentation and testing tasks that should take roughly 1–4 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd Love Your Feedback
&lt;/h2&gt;

&lt;p&gt;I'm particularly interested in how other engineers approach worker ownership and failure recovery.&lt;/p&gt;

&lt;p&gt;A few questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you use lease fencing, visibility timeouts, or another ownership mechanism?&lt;/li&gt;
&lt;li&gt;How do you prevent stale workers from writing outdated state?&lt;/li&gt;
&lt;li&gt;How do you test these failure scenarios under concurrency?&lt;/li&gt;
&lt;li&gt;What trade-offs have you made between delivery guarantees and operational complexity?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working with Go, distributed systems, or background job infrastructure, I'd love to hear how you approach this problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/rajputomsingh/Relay" rel="noopener noreferrer"&gt;github.com/rajputomsingh/Relay&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>go</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
