<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Bun on Kevin Cui's Blog</title><link>http://bugs.cc/tags/bun/</link><description>Recent content in Bun on Kevin Cui's Blog</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><managingEditor>bh@bugs.cc (Kevin Cui)</managingEditor><webMaster>bh@bugs.cc (Kevin Cui)</webMaster><copyright>Kevin Cui (CC BY 4.0)</copyright><lastBuildDate>Tue, 25 Aug 2026 15:00:00 +0800</lastBuildDate><follow_challenge><feedId>67444163872609280</feedId><userId>67028119038586880</userId></follow_challenge><atom:link href="http://bugs.cc/tags/bun/index.xml" rel="self" type="application/rss+xml"/><item><title>Troubleshooting Periodic 100% CPU on a Bun Service</title><link>http://bugs.cc/posts/troubleshooting-bun-kafkajs-cpu-spin/</link><pubDate>Tue, 25 Aug 2026 15:00:00 +0800</pubDate><author>bh@bugs.cc (Kevin Cui)</author><guid>http://bugs.cc/posts/troubleshooting-bun-kafkajs-cpu-spin/</guid><description>&lt;p&gt;A few days ago we hit a fairly interesting production issue. A Bun service was periodically pegging one CPU core across three pods, for exactly 10 minutes each time, then dropping back on its own. The service itself was fine: requests worked, health checks never failed. The CPU graph just looked awful.&lt;/p&gt;&#10;&lt;p&gt;It turned out to be two problems that are each pretty mild on their own, stacked on a specific kernel version and a specific Bun patch release. Writing it down.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A few days ago we hit a fairly interesting production issue. A Bun service was periodically pegging one CPU core across three pods, for exactly 10 minutes each time, then dropping back on its own. The service itself was fine: requests worked, health checks never failed. The CPU graph just looked awful.</p>
<p>It turned out to be two problems that are each pretty mild on their own, stacked on a specific kernel version and a specific Bun patch release. Writing it down.</p>
<p><strong>Environment:</strong></p>
<ul>
<li>Bun 1.3.14 (pinned in the Dockerfile)</li>
<li>kafkajs 2.2.4</li>
<li>Kubernetes node kernel 5.10.134</li>
</ul>
<p>Repro: <a href="https://github.com/BlackHole1/bun-epoll-timer-spin-repro">bun-epoll-timer-spin-repro</a>. I&rsquo;ll come back to this later.</p>
<h2 id="the-symptom">
  <a href="#the-symptom">The symptom</a>
</h2>
<p><img src="/images/troubleshooting-bun-kafkajs-cpu-spin/cpu-day.png" alt="Minute-level CPU of three pods on one day"></p>
<p>Minute-level CPU for the three pods on one day. The vertical lines at the bottom are Kafka produces; more on that later. A few things stand out:</p>
<ul>
<li>The plateau sits at 0.99 cores and never goes above 1.0. Only one thread is busy.</li>
<li>Every stretch is a multiple of 600 seconds. It jumps up and down within a single scrape interval, not a gradual climb.</li>
<li>All three pods often start the stretch together.</li>
<li>Almost nothing overnight. It tracks traffic.</li>
<li>During a spike, user is about 2/3 and sys about 1/3. Network, fd count, and memory look the same as a quiet period.</li>
</ul>
<p>That last point already rules a lot out. We weren&rsquo;t chewing through large payloads, and it wasn&rsquo;t pure JS compute (sys wouldn&rsquo;t be that high). It looked like a busy loop that still makes syscalls.</p>
<p>I first wondered if a worker was stuck in a tight loop on some job. But over 24 hours, every log related to job timeouts, retries, and heartbeats was zero. Busy periods and idle periods produced almost the same amount of logs. It also wasn&rsquo;t introduced by a deploy: every one of the 16 days we have metrics for looks the same.</p>
<h2 id="reading-proc-in-the-pod">
  <a href="#reading-proc-in-the-pod">Reading /proc in the pod</a>
</h2>
<p>Can&rsquo;t poke production casually, and there&rsquo;s no perf in the container, so I <code>kubectl exec</code>&rsquo;d and read <code>/proc</code>. That&rsquo;s fully read-only.</p>
<p>Diff utime/stime of every thread in <code>/proc/&lt;pid&gt;/task/*/stat</code> over 3 seconds and find the hot one. During a spike (100 ticks = one core):</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">tid   comm        d_utime  d_stime
</span></span><span class="line"><span class="cl">8     bun         197      100
</span></span><span class="line"><span class="cl">95    HTTP        0        0
</span></span><span class="line"><span class="cl">9     bun         0        0
</span></span><span class="line"><span class="cl">16    Bun         0        0
</span></span><span class="line"><span class="cl">15    Bun         0        0
</span></span><span class="line"><span class="cl">14    Bun         0        0
</span></span><span class="line"><span class="cl">13    Bun         0        0
</span></span><span class="line"><span class="cl">12    HeapHelper  0        0
</span></span><span class="line"><span class="cl">11    HeapHelper  0        0
</span></span><span class="line"><span class="cl">10    HeapHelper  0        0</span></span></code></pre></div>
<p>The hot thread is the main thread (the one where tid equals pid). Then I sampled <code>/proc/&lt;pid&gt;/task/&lt;tid&gt;/syscall</code> as fast as I could. The first column is the syscall number the thread is currently in, followed by six arguments. When the thread is in userspace, the file says <code>running</code>:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">i</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[</span> <span class="nv">$i</span> -lt <span class="m">3000</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  cat /proc/8/task/8/syscall &gt;&gt; /tmp/sc.txt
</span></span><span class="line"><span class="cl">  <span class="nv">i</span><span class="o">=</span><span class="k">$((</span>i+1<span class="k">))</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="c1"># which syscall it is blocked on</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;{print $1}&#39;</span> /tmp/sc.txt <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn
</span></span><span class="line"><span class="cl"><span class="c1"># epoll_pwait timeout (4th argument, i.e. column 5)</span>
</span></span><span class="line"><span class="cl">awk <span class="s1">&#39;$1 == 281 {print $5}&#39;</span> /tmp/sc.txt <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn</span></span></code></pre></div>
<p>Syscall numbers depend on the architecture. On x86_64 the table is <a href="https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl">syscall_64.tbl</a> in the kernel tree; if the box has auditd, <code>ausyscall x86_64 281</code> works too. These are the ones that show up later:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">202  futex
</span></span><span class="line"><span class="cl">281  epoll_pwait
</span></span><span class="line"><span class="cl">441  epoll_pwait2</span></span></code></pre></div>
<p>On arm64, <code>epoll_pwait</code> is 22. <code>epoll_pwait2</code> is one of the newer syscalls that got a single number across architectures: 441 everywhere.</p>
<p>Idle:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">   2969 281
</span></span><span class="line"><span class="cl">     29 running
</span></span><span class="line"><span class="cl">      2 202
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   1850 0x63
</span></span><span class="line"><span class="cl">    325 0x53
</span></span><span class="line"><span class="cl">    201 0x56
</span></span><span class="line"><span class="cl">    192 0x57
</span></span><span class="line"><span class="cl">     90 0x5d</span></span></code></pre></div>
<p>2969 of 3000 samples were sitting in syscall 281, <code>epoll_pwait</code>, with a timeout of tens to hundreds of milliseconds. Normal. One detail: 441 (<code>epoll_pwait2</code>) never showed up at all. That matters later.</p>
<p>During a spike:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">   2979 running
</span></span><span class="line"><span class="cl">     21 281
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">     21 0x1</span></span></code></pre></div>
<p>Almost entirely in userspace. The rare times it was in <code>epoll_pwait</code>, the timeout was 1ms.</p>
<p>Then <code>/proc/&lt;pid&gt;/net/tcp</code>: 4 connections when idle (Redis, Postgres). During a spike, two extra connections to port 9095, the Kafka broker.</p>
<h2 id="every-spike-was-a-kafka-send">
  <a href="#every-spike-was-a-kafka-send">Every spike was a Kafka send</a>
</h2>
<p>The service publishes metering events with kafkajs. I pulled 48 hours of logs that trigger a Kafka produce and lined them up against 115 CPU stretches:</p>
<ul>
<li>Every stretch starts within -13s to +1s of the nearest produce (within scrape jitter)</li>
<li>Every stretch ends 600s to 611s after the last produce in that stretch</li>
<li>The other way around: all 373 produces fall inside some stretch. None missed.</li>
</ul>
<p>600s is the default <code>connections.max.idle.ms</code> on a Kafka broker: the broker closes a connection after 10 minutes idle. So the length of a CPU stretch is the lifetime of one Kafka connection. The three pods spiked together because one user&rsquo;s batch of jobs landed on three machines, each of which sent a message within a few seconds.</p>
<h2 id="the-1ms-settimeout-in-kafkajs">
  <a href="#the-1ms-settimeout-in-kafkajs">The 1ms setTimeout in kafkajs</a>
</h2>
<p>kafkajs 2.2.4, <code>src/network/requestQueue/index.js</code>:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">checkPendingRequests</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">pending</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="k">this</span><span class="p">.</span><span class="nx">canSendSocketRequestImmediately</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">scheduleCheckPendingRequests</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">scheduleCheckPendingRequests</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kd">let</span> <span class="nx">scheduleAt</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">throttledUntil</span> <span class="o">-</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">throttleCheckTimeoutId</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">pending</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">scheduleAt</span> <span class="o">=</span> <span class="nx">scheduleAt</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">?</span> <span class="nx">scheduleAt</span> <span class="o">:</span> <span class="nx">CHECK_PENDING_REQUESTS_INTERVAL</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="nx">throttleCheckTimeoutId</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">this</span><span class="p">.</span><span class="nx">throttleCheckTimeoutId</span> <span class="o">=</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">      <span class="k">this</span><span class="p">.</span><span class="nx">checkPendingRequests</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span> <span class="nx">scheduleAt</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p><code>checkPendingRequests</code> always calls <code>scheduleCheckPendingRequests</code>. When pending is empty and there&rsquo;s no throttle, that still arms a <code>setTimeout</code>, whose callback calls <code>checkPendingRequests</code> again. <code>throttledUntil</code> starts at -1, so <code>scheduleAt</code> is a huge negative number. Both Node and Bun treat a negative delay as 1ms.</p>
<p>So from the first response on a broker connection, that connection runs a 1ms <code>setTimeout</code> loop until <code>destroy()</code> is called. And <code>destroy()</code> only runs when the connection drops.</p>
<p>This has been reported upstream (<a href="https://github.com/tulios/kafkajs/issues/1556">kafkajs#1556</a>, <a href="https://github.com/tulios/kafkajs/issues/1704">kafkajs#1704</a>). A fix PR has been sitting around for a long time (<a href="https://github.com/tulios/kafkajs/pull/1572">kafkajs#1572</a>) and never landed. We had already patched it in our repo, but the patch only clamped the negative value with <code>Math.max(1, scheduleAt || 1)</code>, which keeps the 1ms loop around instead of killing it.</p>
<p>Still: a 1ms <code>setTimeout</code> loop is maybe 2% CPU on Node. Why does it peg a whole core?</p>
<h2 id="why-bun-pegs-a-whole-core">
  <a href="#why-bun-pegs-a-whole-core">Why Bun pegs a whole core</a>
</h2>
<p>The event loop. Bun 1.3.14, <code>packages/bun-usockets/src/eventing/epoll_kqueue.c</code>:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int</span> <span class="nf">bun_epoll_pwait2</span><span class="p">(</span><span class="kt">int</span> <span class="n">epfd</span><span class="p">,</span> <span class="k">struct</span> <span class="n">epoll_event</span> <span class="o">*</span><span class="n">events</span><span class="p">,</span> <span class="kt">int</span> <span class="n">maxevents</span><span class="p">,</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">timespec</span> <span class="o">*</span><span class="n">timeout</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">has_epoll_pwait2</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ... sys_epoll_pwait2(...)
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">timeoutMs</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">timeout</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">timeoutMs</span> <span class="o">=</span> <span class="n">timeout</span><span class="o">-&gt;</span><span class="n">tv_sec</span> <span class="o">*</span> <span class="mi">1000</span> <span class="o">+</span> <span class="n">timeout</span><span class="o">-&gt;</span><span class="n">tv_nsec</span> <span class="o">/</span> <span class="mi">1000000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">do</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">ret</span> <span class="o">=</span> <span class="nf">epoll_pwait</span><span class="p">(</span><span class="n">epfd</span><span class="p">,</span> <span class="n">events</span><span class="p">,</span> <span class="n">maxevents</span><span class="p">,</span> <span class="n">timeoutMs</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">mask</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="nf">IS_EINTR</span><span class="p">(</span><span class="n">ret</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">ret</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Bun prefers <code>epoll_pwait2</code>, which takes a nanosecond <code>timespec</code>. <code>epoll_pwait2</code> only exists since Linux 5.11; Bun checks the kernel version and falls back to <code>epoll_pwait</code> with a millisecond timeout below that. The bug is <code>tv_nsec / 1000000</code>: it truncates toward zero.</p>
<p>A 1ms timer re-arms itself in its own callback. The event loop computes how long to wait next: 0.9x ms left, truncated to 0, <code>epoll_pwait(…, 0)</code> returns immediately, compute again, still 0, spin until the timer fires. Then the callback arms a new 1ms timer, and around we go.</p>
<p>That matches <code>/proc</code>: the main thread never called <code>epoll_pwait2</code>, because our nodes are 5.10.134. The <code>0x1</code> samples during a spike are the one call per round that actually sleeps. The timeout-0 calls return immediately, so you almost never catch them.</p>
<p>The truncation has been there since 1.3.11. Only 1.3.14 blows up. The difference is <a href="https://github.com/oven-sh/bun/pull/29806">bun#29806</a> in 1.3.14: <code>timespec.now()</code> switched from <code>CLOCK_MONOTONIC_COARSE</code> on Linux to a nanosecond <code>hw_timer</code> (rdtsc). The old clock is jiffy-granularity, milliseconds. Remaining time on a 1ms timer is either a full 1ms or already due. You never see 0.9ms, so truncation never produces 0. After the nanosecond clock, that old truncation finally got hit.</p>
<p>Upstream fixed it in <a href="https://github.com/oven-sh/bun/pull/34780">bun#34780</a> (round up instead). That only shipped in 1.4.0; there were no more 1.3.x releases after 1.3.14. So the only affected version is 1.3.14, and only on kernels without <code>epoll_pwait2</code>.</p>
<h2 id="reproducing-it-locally">
  <a href="#reproducing-it-locally">Reproducing it locally</a>
</h2>
<p>You don&rsquo;t need a 5.10 machine. Docker seccomp can make a syscall return ENOSYS. When Bun sees <code>epoll_pwait2</code> return ENOSYS, it takes the same fallback path:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;defaultAction&#34;</span><span class="p">:</span> <span class="s2">&#34;SCMP_ACT_ALLOW&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;syscalls&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nt">&#34;names&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;epoll_pwait2&#34;</span><span class="p">],</span> <span class="nt">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;SCMP_ACT_ERRNO&#34;</span><span class="p">,</span> <span class="nt">&#34;errnoRet&#34;</span><span class="p">:</span> <span class="mi">38</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>The full repro is in <a href="https://github.com/BlackHole1/bun-epoll-timer-spin-repro">bun-epoll-timer-spin-repro</a>, Docker only:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git clone https://github.com/BlackHole1/bun-epoll-timer-spin-repro.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> bun-epoll-timer-spin-repro
</span></span><span class="line"><span class="cl">./run.sh</span></span></code></pre></div>
<p>It runs Bun 1.3.13 / 1.3.14 / 1.4.0, with and without <code>epoll_pwait2</code>, on three timer setups: a plain <code>setTimeout(fn, 1)</code>, a plain <code>setTimeout(fn, 10)</code>, and constructing kafkajs&rsquo;s <code>RequestQueue</code> then calling <code>checkPendingRequests()</code> once (no real Kafka). Each case runs 5 seconds and reports timer fire rate plus CPU:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">bun       epoll_pwait2   mode        iter/s   user%    sys%
</span></span><span class="line"><span class="cl">1.3.13    available      timer1         330     1.2     1.4
</span></span><span class="line"><span class="cl">1.3.13    available      timer10         84     1.4     0.8
</span></span><span class="line"><span class="cl">1.3.13    available      kafkajs        327     2.2     0.9
</span></span><span class="line"><span class="cl">1.3.13    blocked        timer1         327     1.7     1.1
</span></span><span class="line"><span class="cl">1.3.13    blocked        timer10         84     1.5     1.2
</span></span><span class="line"><span class="cl">1.3.13    blocked        kafkajs        321     2.3     1.5
</span></span><span class="line"><span class="cl">1.3.14    available      timer1         336     1.2     0.7
</span></span><span class="line"><span class="cl">1.3.14    available      timer10         85       1     0.2
</span></span><span class="line"><span class="cl">1.3.14    available      kafkajs        335     2.3     0.7
</span></span><span class="line"><span class="cl">1.3.14    blocked        timer1         996      56    43.5
</span></span><span class="line"><span class="cl">1.3.14    blocked        timer10         92       2     0.7
</span></span><span class="line"><span class="cl">1.3.14    blocked        kafkajs        993    62.9    36.6
</span></span><span class="line"><span class="cl">1.4.0     available      timer1         334     1.3     1.6
</span></span><span class="line"><span class="cl">1.4.0     available      timer10         86     0.8     0.4
</span></span><span class="line"><span class="cl">1.4.0     available      kafkajs        337       2     1.3
</span></span><span class="line"><span class="cl">1.4.0     blocked        timer1         338       1     1.1
</span></span><span class="line"><span class="cl">1.4.0     blocked        timer10         85     0.7     0.5
</span></span><span class="line"><span class="cl">1.4.0     blocked        kafkajs        334     1.3     1.1</span></span></code></pre></div>
<p>Only 1.3.14 with the syscall blocked pegs a core. So this isn&rsquo;t really about kafkajs. Any 1ms <code>setTimeout</code> loop does the same thing on that combination.</p>
<p>strace over 2 seconds: 148k times <code>epoll_pwait(4, [], 1024, 0, [], 8) = 0 &lt;0.000001&gt;</code>, 6 to 7 µs apart. That&rsquo;s the 2/3 user + 1/3 sys.</p>
<h2 id="the-fix">
  <a href="#the-fix">The fix</a>
</h2>
<p>Patch kafkajs: if pending is empty and there&rsquo;s no throttle, don&rsquo;t schedule a timer. Everything else stays.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">       if (this.pending.length &gt; 0) {
</span></span><span class="line"><span class="cl">         scheduleAt = scheduleAt &gt; 0 ? scheduleAt : CHECK_PENDING_REQUESTS_INTERVAL
</span></span><span class="line"><span class="cl">       }
</span></span><span class="line"><span class="cl"><span class="gd">-      // Prevent negative or invalid delays
</span></span></span><span class="line"><span class="cl"><span class="gd">-      scheduleAt = Math.max(1, scheduleAt || 1)
</span></span></span><span class="line"><span class="cl"><span class="gi">+      if (scheduleAt &lt;= 0) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        return
</span></span></span><span class="line"><span class="cl"><span class="gi">+      }
</span></span></span><span class="line"><span class="cl">       this.throttleCheckTimeoutId = setTimeout(() =&gt; {
</span></span></code></pre></div>
<p>In the repro, CPU went from 99% to 0.2%.</p>
<p>Two extra things while I was there:</p>
<ul>
<li>Pinned kafkajs from <code>^2.2.4</code> to exact <code>2.2.4</code>. Bun&rsquo;s <code>patchedDependencies</code> matches on the key <code>kafkajs@2.2.4</code>. If the lockfile resolves a different version, Bun silently skips the patch, <code>bun install</code> exits 0, and it doesn&rsquo;t say a word.</li>
<li>Added a test that imports <code>kafkajs/src/network/requestQueue/index.js</code> directly and asserts that an empty pending queue does not arm a timer. If the patch ever disappears, the test fails first.</li>
</ul>
<p>The patch is already in production. The periodic one-core spikes are gone. Once we move to Bun 1.4.0, this combination won&rsquo;t come back even if the patch gets dropped.</p>
]]></content:encoded></item></channel></rss>