<?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: evan</title>
    <description>The latest articles on DEV Community by evan (@evannn-dev).</description>
    <link>https://dev.to/evannn-dev</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%2F4175773%2F63b9c094-cc02-448a-aefd-8bebfa49b7a3.png</url>
      <title>DEV Community: evan</title>
      <link>https://dev.to/evannn-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evannn-dev"/>
    <language>en</language>
    <item>
      <title>Why Unbounded Worker Pools Blow Up Go Services Under Back‑pressure</title>
      <dc:creator>evan</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:59:25 +0000</pubDate>
      <link>https://dev.to/evannn-dev/why-unbounded-worker-pools-blow-up-go-services-under-back-pressure-2jm4</link>
      <guid>https://dev.to/evannn-dev/why-unbounded-worker-pools-blow-up-go-services-under-back-pressure-2jm4</guid>
      <description>&lt;h3&gt;
  
  
  problem statement
&lt;/h3&gt;

&lt;p&gt;in a recent deployment we added a background worker pool to a high‑traffic HTTP service. the pool pulled jobs from a shared channel without any limit on the number of goroutines. under normal load the service seemed fine, but a spike in upstream traffic caused the request queue to fill, the pool spawned thousands of goroutines, and the pod hit OOM kills within seconds. the crash cascaded to the entire deployment because the readiness probe never recovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  why the pool exploded
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;no back‑pressure&lt;/strong&gt; – a plain &lt;code&gt;chan Job&lt;/code&gt; without a buffer or a semaphore allows any number of producers to push work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;missing context propagation&lt;/strong&gt; – workers ignored the request context, so they kept processing even after the client timed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;absence of telemetry&lt;/strong&gt; – only occasional log lines were emitted; the CPU and memory usage spiked unnoticed until the pod was evicted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  a minimal reproducible snippet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;startWorkerPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// &amp;lt;- unbounded loop&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// heavy processing, no ctx&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;job&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// unbuffered, producers block only on send&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;startWorkerPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/enqueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// simulate burst of 10k requests per second&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusAccepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  production‑grade fixes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;bulkhead with semaphore&lt;/strong&gt; – limit concurrent workers to a sane number (e.g., &lt;code&gt;runtime.GOMAXPROCS()*2&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bounded channel or work‑queue&lt;/strong&gt; – use a buffered channel and drop or back‑pressure when the buffer is full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context‑aware workers&lt;/strong&gt; – pass &lt;code&gt;r.Context()&lt;/code&gt; downstream; abort processing when the client disconnects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;metrics &amp;amp; alerts&lt;/strong&gt; – expose &lt;code&gt;goroutine&lt;/code&gt;, &lt;code&gt;heap&lt;/code&gt;, and queue length via Prometheus; set alerts on 80% memory usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chaos testing&lt;/strong&gt; – inject artificial latency on the upstream API and verify that the pool respects the limits without OOM.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  trade‑offs to consider
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;throughput vs latency&lt;/strong&gt; – a tighter semaphore reduces peak throughput but guarantees latency bounds and prevents node eviction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;queue size&lt;/strong&gt; – a small buffer protects the node but may increase request rejection rates; choose a size based on expected burst and acceptable 503 rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cost of retries&lt;/strong&gt; – if you drop jobs, ensure idempotent downstream processing or a durable retry store (e.g., Kafka) to avoid data loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;observability overhead&lt;/strong&gt; – adding per‑request tracing adds CPU, but without it you cannot see the back‑pressure chain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  conclusion
&lt;/h3&gt;

&lt;p&gt;unbounded worker pools are a classic hidden failure mode that surfaces only under load spikes. limiting concurrency, propagating cancellation, and instrumenting the queue are non‑negotiable in production. how does your stack enforce bulkhead isolation for background processing, and what metrics do you rely on to detect a runaway goroutine storm?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>go</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
