<?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: Aniket Misra</title>
    <description>The latest articles on DEV Community by Aniket Misra (@aniket_misra_e47d1564ab7b).</description>
    <link>https://dev.to/aniket_misra_e47d1564ab7b</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%2F3898086%2F9a9ac2ea-daea-42ff-8ba1-17ae767ab2f6.png</url>
      <title>DEV Community: Aniket Misra</title>
      <link>https://dev.to/aniket_misra_e47d1564ab7b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aniket_misra_e47d1564ab7b"/>
    <language>en</language>
    <item>
      <title>When Your Security Tool Says "LOOKS SAFE" (TxnLense, Part 2)</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Sun, 04 Oct 2026 18:17:36 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/when-your-security-tool-says-looks-safe-txnlense-part-2-4pkg</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/when-your-security-tool-says-looks-safe-txnlense-part-2-4pkg</guid>
      <description>&lt;p&gt;Part 1 ended on the first transaction I ever ran through TxnLense end to end: an unlimited ERC-20 approval, the single most-cited pattern behind wallet-draining attacks, and the exact scenario the extension exists to catch. It decoded correctly. Function name, spender, amount: all correct, the amount displayed as &lt;code&gt;MAX (unlimited)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The badge across the top said &lt;strong&gt;LOOKS SAFE&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This part is about why, what else I found sitting next to it once I started actually reading the code instead of trusting the pipeline I'd just finished verifying, and what a tool like this can and can't honestly claim once you've seen it fail once.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a verdict gets made
&lt;/h2&gt;

&lt;p&gt;Decoding and risk-flagging are separate stages on purpose. The decoder's only job is to turn a hex blob into structured data: function name, arguments, a list of "token flows" (who sends what to whom). The risk engine never looks at raw calldata. It only looks at what the decoder already produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Rule 1 — unlimited token approval&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;approvalFlow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenFlows&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;∞&lt;/span&gt;&lt;span class="dl"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;approvalFlow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unlimited Approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are granting unlimited &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;approvalFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; spending rights. …`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that rule on its own and it's correct. If a flow exists whose symbol contains &lt;code&gt;"approval"&lt;/code&gt; and whose amount is the infinity symbol, flag it high. The question the rule never asks, because it isn't its job to ask, is: &lt;em&gt;did a flow get built at all?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the flow comes from
&lt;/h2&gt;

&lt;p&gt;That's the decoder's job, in &lt;code&gt;extractTokenFlows&lt;/code&gt;. Before my fix, the approve branch looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_TOKENS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;functionName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;tokenMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spenderParam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spender&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amountParam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spenderParam&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;amountParam&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="cm"&gt;/* … */&lt;/span&gt;
      &lt;span class="na"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenMeta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; approval`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="cm"&gt;/* … */&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;KNOWN_TOKENS&lt;/code&gt; is a hardcoded map, a handful of well-known mainnet addresses to their symbol and decimals. &lt;code&gt;tokenMeta&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; for anything not on that list, and the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; in the &lt;code&gt;if&lt;/code&gt; means the entire block, the entire flow, is skipped when it is. Call &lt;code&gt;approve&lt;/code&gt; on a token nobody's heard of and this function returns nothing, every time, no matter what the amount is.&lt;/p&gt;

&lt;p&gt;Here's the test case I ran it against, from Part 1's mock wallet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethereum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_sendTransaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAKE_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeApprove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FAKE_SPENDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_UINT256&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FAKE_TOKEN&lt;/code&gt; is a made-up address. It was never going to be in &lt;code&gt;KNOWN_TOKENS&lt;/code&gt;. The decoder found nothing, &lt;code&gt;tokenFlows&lt;/code&gt; came back empty, and the risk rule — correctly, by its own logic — had nothing to find.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "found nothing" and "couldn't look" are not the same answer
&lt;/h2&gt;

&lt;p&gt;Rule 1 is written as if an empty result always means "no unlimited approval here." The allowlist gate means an empty result can also mean "I never checked, because I didn't recognize the token." The function returns the same shape either way, so nothing downstream can tell the two apart. The badge collapses them into one label: safe.&lt;/p&gt;

&lt;p&gt;That gap is a bigger deal here than it would be in most software, because of who's likely to be on the other side of it. The hardcoded list covers tokens everyone already knows to trust. The dangerous case, almost by construction, is the unfamiliar one: an attacker's own token, or a brand-new deployment nobody's added to any list yet. The allowlist was most likely to fail exactly where the tool mattered most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and what I had to check twice
&lt;/h2&gt;

&lt;p&gt;The fix removes the gate and falls back to a generic label when the token isn't recognized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_TOKENS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown Token&lt;/span&gt;&lt;span class="dl"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;functionName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// … builds the flow unconditionally now&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The amount and the spender were always fully decoded; only the pretty symbol was ever in question. Displaying "Token approval" instead of "USDC approval" costs nothing. Returning no flag at all for a genuinely unlimited approval cost everything the feature was built for.&lt;/p&gt;

&lt;p&gt;I didn't trust that reasoning on its own, so I ran the fixed function directly, the same way I'd read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;evaluateRisk&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tokenFlows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;∞&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → { riskLevel: 'high', riskFlags: [{ label: 'Unlimited Approval', … }] }&lt;/span&gt;

&lt;span class="nf"&gt;evaluateRisk&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ethFlow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ordinary 0.01 ETH transfer */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → { riskLevel: 'safe', riskFlags: [] }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second call matters as much as the first. A fix that makes everything flag high is just a different kind of broken, so the control case, the same one from Part 1's mock harness, has to still come back clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more silent failures, found the same way
&lt;/h2&gt;

&lt;p&gt;Once I stopped trusting "it compiled" as a stand-in for "it works," two more problems turned up nearby, both sharing the exact shape of the one above: something fails quietly, and a &lt;code&gt;try/catch&lt;/code&gt; or a missing build step hides it from view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Buffer&lt;/code&gt; doesn't exist in a service worker.&lt;/strong&gt; The function that decodes a transaction's revert reason used &lt;code&gt;Buffer.from(hex, 'hex').toString('utf8')&lt;/code&gt; to turn the raw return data into readable text. &lt;code&gt;Buffer&lt;/code&gt; is a Node.js global. The background script runs as an actual Manifest V3 service worker, a browser environment, not Node, and &lt;code&gt;Buffer&lt;/code&gt; is simply undefined there. Every call threw, every throw landed in a surrounding &lt;code&gt;catch&lt;/code&gt;, and the UI fell back to a generic message: "transaction would revert," with no reason. The feature had never once worked, and nothing said so. The fix swaps in &lt;code&gt;TextDecoder&lt;/code&gt;, the browser-native equivalent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project had never actually been typechecked.&lt;/strong&gt; &lt;code&gt;vite build&lt;/code&gt; uses esbuild to transpile TypeScript, and esbuild strips types without checking them: that's what makes it fast. There was no separate &lt;code&gt;tsc --noEmit&lt;/code&gt; step anywhere in the project's scripts, so none of this had ever been verified against the type checker it was written with. Once I added that script and ran it, it surfaced the &lt;code&gt;Buffer&lt;/code&gt; issue again, from a different angle: &lt;code&gt;Cannot find name 'Buffer'&lt;/code&gt;, a declaration-level version of the exact bug above. The fact that both paths pointed at the same line independently is a decent argument that the bug was real rather than a one-off. It also meant &lt;code&gt;window.ethereum&lt;/code&gt;'s type had never been declared anywhere in the codebase, and &lt;code&gt;vite.config.ts&lt;/code&gt;'s own Node-only APIs (&lt;code&gt;__dirname&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;) were leaking into the same type-checking pass as the browser code, which is the general version of the &lt;code&gt;Buffer&lt;/code&gt; mistake: nothing was stopping a Node-only API from being used somewhere it would only ever fail silently at runtime.&lt;/p&gt;

&lt;p&gt;None of these three bugs would show up in a demo. They all share the same failure mode: something that looks like a result, isn't flagged as suspicious, and is wrong. A missing flag doesn't look different from an absent one. A caught exception doesn't look different from success. A green build doesn't look different from a checked one. The demo only exercises the paths you decided to click through, and "it compiled" was never actually evidence that any of this worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should actually be on the badge
&lt;/h2&gt;

&lt;p&gt;The deeper issue outlives the specific bug. "LOOKS SAFE" is a single, confident word standing in for two situations that should never share a label:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The decoder understood the call completely, and none of the five active rules fired.&lt;/li&gt;
&lt;li&gt;The decoder couldn't fully make sense of the call, so there was nothing to check it against.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A reassuring label is the wrong output for the second case. It isn't a finding, it's the absence of one, and treating absence of evidence as evidence of safety is exactly how an attacker's own unlisted token walked through the version I'd just built with the badge intact.&lt;/p&gt;

&lt;p&gt;I haven't shipped a fix for this yet, and I don't want to claim otherwise. The honest version is a tri-state verdict: risky, no known risks found, or couldn't fully evaluate this. Even just renaming the label from "LOOKS SAFE" to "NO KNOWN RISKS FOUND" would be a real improvement with no code change at all, because it stops implying a judgment the tool didn't actually make.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second asymmetry, caught while writing this post
&lt;/h2&gt;

&lt;p&gt;Rereading the risk rules for this post, rather than just to patch the one bug I already knew about, turned up something I hadn't looked for: the unlimited-amount check only exists for one of the two approval mechanisms TxnLense decodes.&lt;/p&gt;

&lt;p&gt;A plain EIP-2612 &lt;code&gt;Permit&lt;/code&gt; with the maximum value gets exactly the escalation you'd want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;evaluateRisk&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;typedDataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Permit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;typedDataMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MAX_UINT256&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → riskLevel: 'high', label: 'Unlimited Permit Signature'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Permit2 &lt;code&gt;PermitSingle&lt;/code&gt; with the same maximum amount does not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;evaluateRisk&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;typedDataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PermitSingle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;typedDataMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MAX_UINT160&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="c1"&gt;// → riskLevel: 'medium', label: 'Permit2 Signature'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Permit2 rule flags every Permit2 signature at a flat "medium," with a generic "verify the spender and expiration," regardless of amount. It never reads &lt;code&gt;details.amount&lt;/code&gt; at all. An unlimited Permit2 grant, structurally the same risk as the unlimited approval this whole post is about, gets less visual weight than a five-dollar one. I haven't fixed this yet either. It's going in the next pass, and I'm naming it here rather than quietly patching it before anyone noticed, for the same reason as the badge: a post about a tool overstating what it checked shouldn't itself overstate what's already fixed.&lt;/p&gt;

&lt;p&gt;One more small thing worth knowing if you read the source: the risk levels are &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, and &lt;code&gt;safe&lt;/code&gt;, and the popup has styling for all four. But every rule that pushes a flag currently sets its level to &lt;code&gt;high&lt;/code&gt; or &lt;code&gt;medium&lt;/code&gt; explicitly. There's no path through the current rules that produces &lt;code&gt;low&lt;/code&gt; — it's reachable in the type and in the UI, not in the logic. Not a bug exactly, since nothing breaks, but worth knowing the fourth state is aspirational right now, not active.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this can't promise, even once every bug above is fixed
&lt;/h2&gt;

&lt;p&gt;Writing all of this down is also a reasonable time to be honest about the ceiling, not just the bugs under it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs in the same world it's watching.&lt;/strong&gt; The interception patch lives in the page's own JavaScript context, which Part 1 covered as the reason it can see &lt;code&gt;window.ethereum&lt;/code&gt; at all. The flip side is that a sufficiently hostile page shares that same context and could, in principle, detect or interfere with a patch running alongside it. "Informational only, never blocks" lowers the stakes of a false positive. It doesn't make the patch invisible to the page it's sitting on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It only patches &lt;code&gt;window.ethereum&lt;/code&gt;.&lt;/strong&gt; A newer connection pattern, EIP-6963, lets multiple wallets announce themselves without all of them colliding on that one global. I haven't checked whether a dapp using that pattern exclusively would ever call through the object TxnLense patches, or whether it would route around it entirely. I'm flagging it as an open question rather than a confirmed gap, because I'd rather say "I don't know yet" than quietly assume coverage I haven't verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Function names come from a public, user-submitted database.&lt;/strong&gt; When calldata doesn't decode against a known ABI, the fallback looks up the selector on 4byte.directory, which is editable by anyone. A wrong or malicious submission there would come back as a wrong name here. It's a reasonable fallback for a project at this stage, and it's not infallible in a way worth remembering before trusting an unfamiliar function name at face value.&lt;/p&gt;

&lt;p&gt;None of these are reasons not to build the thing. They're the difference between "this extension flags some real risks" and "this extension flags all risk," and that difference is exactly what a badge that just says "LOOKS SAFE" was quietly erasing.&lt;/p&gt;

&lt;p&gt;The through-line across both of these posts, and honestly across this whole run of writing about my own bugs, is the same each time: the parts that look finished are the parts I'd stopped checking, not the parts that were actually correct. The fix was never cleverness. It was going back and checking the part I'd already decided was fine.&lt;/p&gt;

&lt;p&gt;If you want to look yourself rather than take any of this on faith: &lt;a href="https://github.com/Aniket98Misra/TxnLense" rel="noopener noreferrer"&gt;github.com/Aniket98Misra/TxnLense&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>Intercepting a Wallet from a Browser Extension (TxnLense, Part 1)</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Sun, 04 Oct 2026 18:12:43 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/intercepting-a-wallet-from-a-browser-extension-txnlense-part-1-52bg</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/intercepting-a-wallet-from-a-browser-extension-txnlense-part-1-52bg</guid>
      <description>&lt;p&gt;Every time you sign something, an application asked your wallet to do it through a single JavaScript call, and your wallet showed you its interpretation of that call. Sometimes the interpretation is good. Often it's a hex blob and a confirm button. I wrote about the standards effort to fix this at the source in &lt;a href="https://dev.to/aniket_misra_e47d1564ab7b/the-end-of-blind-signing-deep-diving-into-erc-7730-erc-8213-and-clear-signing-2af0"&gt;The End of Blind Signing&lt;/a&gt;: ERC-7730, clear signing, wallets and dapps agreeing on a way to describe what a signature does.&lt;/p&gt;

&lt;p&gt;TxnLense is the other approach: what can you do &lt;em&gt;today&lt;/em&gt;, without waiting for any wallet or dapp to adopt anything? Sit between the two. Read the call as it goes by, decode it, and tell the user in plain language what they're about to authorize.&lt;/p&gt;

&lt;p&gt;This is a two-part series. Part 1 is about how interception works in a Manifest V3 extension, and the long, instructive way I got it working, including how I tested a wallet-interception extension on a machine that couldn't run a wallet. Part 2 is about the part I trust least in hindsight: the moment the decoder confidently told me something dangerous looked safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does, and what it deliberately doesn't
&lt;/h2&gt;

&lt;p&gt;The pipeline is four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Intercept&lt;/strong&gt;: catch &lt;code&gt;eth_sendTransaction&lt;/code&gt;, &lt;code&gt;eth_signTypedData&lt;/code&gt;, and &lt;code&gt;eth_signTypedData_v4&lt;/code&gt; calls as the page makes them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode&lt;/strong&gt;: turn calldata into a function name and named arguments, and typed-data payloads (including Permit2) into something readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flag&lt;/strong&gt;: run a small set of deterministic rules over the result: unlimited approvals, unlimited permits, high-value transfers, unrecognized functions, Permit2 signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show&lt;/strong&gt;: render all of it in the extension popup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The design decision I care most about is what it &lt;em&gt;doesn't&lt;/em&gt; do: it never blocks, alters, or delays the call. The patched function records what it saw and then hands the original arguments to the original wallet function, unmodified. Version 0.1 is informational only.&lt;/p&gt;

&lt;p&gt;That's a deliberate choice. A third-party extension with the power to veto a signature is a much heavier thing to ask a user to trust than one that only reads, and a rules-based flagger &lt;em&gt;will&lt;/em&gt; have false positives; a false positive that silently blocks a legitimate transaction is a worse failure than a warning the user can read and ignore. Observing is a smaller, more defensible promise, and the code makes it structurally obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;originalRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// always. never blocks, never mutates.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything hard about this project comes from a much less glamorous problem: getting your code to run in the right place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fact that shapes the whole architecture
&lt;/h2&gt;

&lt;p&gt;When a wallet like MetaMask is installed, it injects an object at &lt;code&gt;window.ethereum&lt;/code&gt; in the page. Every dapp talks to it through &lt;code&gt;window.ethereum.request({ method, params })&lt;/code&gt;. That's the interface (it's specified in EIP-1193), and it's the seam TxnLense hooks: replace &lt;code&gt;request&lt;/code&gt; with a wrapper that observes and then forwards.&lt;/p&gt;

&lt;p&gt;The catch is that a browser extension's code can run in three different places, and they can't all see the same things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Can see &lt;code&gt;window.ethereum&lt;/code&gt;?&lt;/th&gt;
&lt;th&gt;Can use &lt;code&gt;chrome.runtime&lt;/code&gt;?&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;The page itself&lt;/strong&gt; ("MAIN world")&lt;/td&gt;
&lt;td&gt;Yes, it's the same JS realm the wallet writes into&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Patching the provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Content script&lt;/strong&gt; ("isolated world")&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt;, it shares the DOM but gets its own JavaScript globals&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Relaying messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service worker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No DOM at all&lt;/td&gt;
&lt;td&gt;Yes, plus network access&lt;/td&gt;
&lt;td&gt;Decoding, simulation, storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That middle row is the one that trips everyone. A content script and the page share the same DOM, but they get separate JavaScript worlds, so a variable the page defines is invisible to the content script even though both are "on the page." Isolation is a security feature: it stops a hostile page from reaching into an extension's variables. It's also why a content script's &lt;code&gt;window.ethereum&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; even when the wallet is sitting right there.&lt;/p&gt;

&lt;p&gt;So the job splits along those lines. The provider has to be patched from the page's world, but only the isolated world and the service worker can talk to the extension. Two pieces of code, one message boundary between them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   PAGE (main world)              CONTENT SCRIPT (isolated)         SERVICE WORKER
   ─────────────────              ─────────────────────────         ──────────────
   dapp calls request()
   patched wrapper ──postMessage──▶ content.js ──sendMessage──▶  decode → flag → simulate
                                        │                              │
                                  store for popup  ◀──────────── response ┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;window.postMessage&lt;/code&gt; is the only thing that crosses the page-to-isolated boundary, and &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; is the only thing that crosses to the service worker. Nothing else connects them. Once you see it as three rooms and two doors, the architecture stops being mysterious. It also tells you exactly what a bug in any one of the rooms will look like from the others: silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version, and why it couldn't have worked
&lt;/h2&gt;

&lt;p&gt;The version I started with put nearly everything in one content script. It patched &lt;code&gt;window.ethereum&lt;/code&gt;, it called &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; to reach the worker, and to bridge worlds it built a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; element with inline code as text and appended it to the page, relaying events through a &lt;code&gt;CustomEvent&lt;/code&gt;. The manifest declared that one file with &lt;code&gt;"world": "MAIN"&lt;/code&gt;, so it would run in the page's context.&lt;/p&gt;

&lt;p&gt;If you read the table above, that plan has a contradiction in it: the file needs &lt;code&gt;chrome.runtime&lt;/code&gt; (isolated world) and &lt;code&gt;window.ethereum&lt;/code&gt; (main world) at the same time. No single world offers both. The design leaned on two different tricks to paper over that, an inline script and a manifest flag, and both of them turned out to be wrong for reasons I'd only find out by running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five failures, in order
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Loading the wrong folder
&lt;/h3&gt;

&lt;p&gt;The first error wasn't even in the interception logic. Loading the project root as an unpacked extension failed with &lt;code&gt;Could not load javascript 'content.js' for content script&lt;/code&gt;. The manifest at the root referred to files that existed only in the build output, &lt;code&gt;dist/&lt;/code&gt;. Loading &lt;code&gt;dist/&lt;/code&gt; fixed it, but it left two copies of &lt;code&gt;manifest.json&lt;/code&gt; in the project that could quietly drift apart, which is a problem I'd meet again in Part 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The CSP wall
&lt;/h3&gt;

&lt;p&gt;With the extension loaded, I opened the test page and triggered a transaction. Nothing happened: no popup update, no service-worker activity. But the extension's Errors panel had something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refused to execute inline script because it violates the following Content
Security Policy directive: "script-src 'self' 'wasm-unsafe-eval'".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That policy is Manifest V3's default: no inline script, only files from the extension itself. The culprit was the inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; my content script built from a string. The fix for &lt;em&gt;that&lt;/em&gt; was to delete the bridge, which turned out to be solving a problem that doesn't exist: &lt;code&gt;postMessage&lt;/code&gt; already crosses between worlds natively, so a &lt;code&gt;CustomEvent&lt;/code&gt; relay through injected inline code was never needed.&lt;/p&gt;

&lt;p&gt;I made that change, reloaded, and the error disappeared. The extension still did nothing at all.&lt;/p&gt;

&lt;p&gt;That was the first useful lesson: &lt;strong&gt;the absence of an error is not the absence of a bug.&lt;/strong&gt; The CSP error had been the loud symptom, and clearing it just exposed the quiet one underneath.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Silence, and the decision to instrument
&lt;/h3&gt;

&lt;p&gt;At this point I had a manifest with two scripts, one supposedly running in the page world (&lt;code&gt;inject.js&lt;/code&gt;, declared &lt;code&gt;"world": "MAIN"&lt;/code&gt;) and one relay in the isolated world. No errors anywhere. I checked the popup, the service-worker console, the Errors panel, and the network tab. Nothing.&lt;/p&gt;

&lt;p&gt;When you can't tell which of several components is failing, the fix isn't more staring. It's making silence impossible. I rewrote &lt;code&gt;inject.js&lt;/code&gt; so it logged at every step: on load, on every poll attempt for &lt;code&gt;window.ethereum&lt;/code&gt;, on success, on failure. Then I learned something that cost me a while: &lt;strong&gt;code running in the page's world logs to the page's console, not the extension's.&lt;/strong&gt; I'd been checking every extension console and none of them would ever show it.&lt;/p&gt;

&lt;p&gt;Opening DevTools on the test page itself finally showed something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[txlens] inject.js: file is executing, top of file
[txlens] inject.js: starting to poll for window.ethereum
[txlens] inject.js: poll attempt 0, window.ethereum = undefined
[harness] script block starting
[harness] window.ethereum assigned successfully: {isMetaMask: true, …}
[txlens] inject.js: poll attempt 1, window.ethereum = undefined
…
[txlens] inject.js: poll attempt 20, window.ethereum = undefined
[txlens] inject.js: gave up after 20 attempts, window.ethereum never appeared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script was running, and running exactly as written. The polling loop even survived the expected race: it started before the page's own script had set the provider, which is normal at &lt;code&gt;document_start&lt;/code&gt;. But &lt;em&gt;after&lt;/em&gt; the page logged that it had assigned &lt;code&gt;window.ethereum&lt;/code&gt;, the extension kept reading &lt;code&gt;undefined&lt;/code&gt; for the rest of its two-second window. Typing &lt;code&gt;console.log(window.ethereum)&lt;/code&gt; by hand in the console printed the object.&lt;/p&gt;

&lt;p&gt;Same name, same page, two different answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The context dropdown
&lt;/h3&gt;

&lt;p&gt;DevTools has a small dropdown at the top of the Console panel, defaulting to &lt;code&gt;top&lt;/code&gt;, that selects which JavaScript context your expressions run in. I'd never had a reason to open it. When I did, it listed &lt;code&gt;top&lt;/code&gt; (the page) and, underneath, three separate entries labelled &lt;code&gt;txlens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I selected each one and ran &lt;code&gt;window.ethereum.__probe = 'hello'&lt;/code&gt;. In &lt;code&gt;top&lt;/code&gt;, it worked. In every &lt;code&gt;txlens&lt;/code&gt; entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uncaught TypeError: Cannot set properties of undefined (setting '__probe')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's proof. The extension's script wasn't in the page's world. It was in an isolated one, looking at a &lt;code&gt;window&lt;/code&gt; that had never heard of the wallet. (I don't know exactly why there were three entries. Presumably one per script or context. It didn't matter: none of them could see the provider.)&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The actual cause, and the wrong theory I had first
&lt;/h3&gt;

&lt;p&gt;My first theory was that Chromium's MAIN-world support was flaky. When three JavaScript contexts disagree about a global and nothing throws, "browser bug" is a natural conclusion. It was wrong.&lt;/p&gt;

&lt;p&gt;The real cause was the machine. I develop on Windows 8.1, where Chrome and Edge both stop at version 109. The manifest-level &lt;code&gt;"world": "MAIN"&lt;/code&gt; option shipped in &lt;strong&gt;Chrome 111&lt;/strong&gt;, and &lt;a href="https://groups.google.com/a/chromium.org/g/chromium-extensions/c/_zKyp9XvIzY" rel="noopener noreferrer"&gt;older versions silently ignore the key&lt;/a&gt;. No warning, no error, no partial support. My "page-world" script had simply been running as an ordinary isolated content script the whole time. That fits every observation: the script executed, the polls ran, the provider was invisible.&lt;/p&gt;

&lt;p&gt;There's a symmetry in this that I find funny in hindsight. On my old browser, the original design failed because the flag was ignored. On a modern browser it would have failed &lt;em&gt;differently&lt;/em&gt;: once &lt;code&gt;"world": "MAIN"&lt;/code&gt; is honored, the file has no &lt;code&gt;chrome.runtime&lt;/code&gt;, so the very first &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; would throw. Same design, opposite failure, same root mistake, which was asking one file to live in two worlds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: two files, and a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag with a &lt;code&gt;src&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The version that works separates the two jobs completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;inject.js&lt;/code&gt;&lt;/strong&gt; runs in the page and does only what only the page can do: patch the provider and post a message. No &lt;code&gt;chrome.*&lt;/code&gt; calls anywhere in it. (Excerpts here are trimmed for length.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;interceptProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethereum&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;__txlens_patched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PASSTHROUGH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;INTERCEPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;originalRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ETH_TX_INTERCEPTED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;interceptType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_sendTransaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_sendTransaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_signTypedData&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;__txlens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;originalRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// never blocks, never mutates&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;__txlens_patched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;content.js&lt;/code&gt;&lt;/strong&gt; runs in the isolated world, where &lt;code&gt;chrome.runtime&lt;/code&gt; lives. It has two jobs: get &lt;code&gt;inject.js&lt;/code&gt; into the page, and relay what comes back (the return path, which stores the decoded result for the popup, is omitted here).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inject.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;__txlens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// → service worker&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the manifest stops declaring &lt;code&gt;inject.js&lt;/code&gt; as a content script at all. Instead it makes it &lt;em&gt;fetchable&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"content_scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;all_urls&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"run_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document_start"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"web_accessible_resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"inject.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;all_urls&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does this work when the inline version didn't? A &lt;code&gt;&amp;lt;script src="chrome-extension://…/inject.js"&amp;gt;&lt;/code&gt; isn't inline code. It's an external file load, so the CSP has nothing to refuse. And because the &lt;em&gt;page&lt;/em&gt; parses and runs that tag, the code lands in the page's real JavaScript world regardless of which Chrome version is running. It doesn't depend on &lt;code&gt;"world": "MAIN"&lt;/code&gt;, so it works on my Chrome 109 exactly as it does on a current release. That's the older technique, and it ended up being the more portable one.&lt;/p&gt;

&lt;p&gt;Two costs come with it. The first is that &lt;code&gt;inject.js&lt;/code&gt; loads asynchronously, so it can lose a race with the page. The polling handles a wallet that isn't there &lt;em&gt;yet&lt;/em&gt;, but not a dapp that captured a reference to &lt;code&gt;request&lt;/code&gt; before the patch landed. The second is that a web-accessible resource can be fetched by any page that matches, so a site can probe for it and learn that TxnLense is installed. Both go on Part 2's list of things this can't hide.&lt;/p&gt;

&lt;p&gt;I reloaded, refreshed the test page, clicked a button, clicked the toolbar icon, and the popup showed a decoded &lt;code&gt;approve&lt;/code&gt; call for the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing an interceptor without a wallet
&lt;/h2&gt;

&lt;p&gt;There was one more constraint worth writing about, because it changed how I tested. Current MetaMask releases require Chrome 123 or newer. My browser tops out at 109. I couldn't run the wallet I was building around.&lt;/p&gt;

&lt;p&gt;The workaround came from looking at the seam again. TxnLense doesn't care &lt;em&gt;which&lt;/em&gt; wallet is on the other side. It only cares that &lt;code&gt;window.ethereum&lt;/code&gt; exists and has a &lt;code&gt;request&lt;/code&gt; method. EIP-1193 is the entire contract. Which means anything that implements &lt;code&gt;request&lt;/code&gt; is a wallet as far as the extension can tell. So I wrote one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethereum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;isMetaMask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// some detection logic checks for this&lt;/span&gt;
  &lt;span class="na"&gt;selectedAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAKE_ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt;&amp;gt; ethereum.request  method=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_requestAccounts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;FAKE_ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_sendTransaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// fake tx hash&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eth_signTypedData_v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// fake signature&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nf"&gt;removeListener&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Around that mock is a single HTML page with five buttons, each firing one scenario at it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;code&gt;approve(spender, uint256.max)&lt;/code&gt; call: the unlimited-approval case&lt;/li&gt;
&lt;li&gt;A Permit2 &lt;code&gt;PermitSingle&lt;/code&gt; typed-data signature with the maximum amount&lt;/li&gt;
&lt;li&gt;A native transfer of 5 ETH&lt;/li&gt;
&lt;li&gt;Calldata with a selector nothing recognizes&lt;/li&gt;
&lt;li&gt;An ordinary 0.01 ETH transfer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fifth one is a &lt;strong&gt;control&lt;/strong&gt;, and it matters as much as the other four. A flagger that warns on everything isn't cautious, it's broken, and you can only see that if you also feed it something that should pass clean. The harness serves over &lt;code&gt;http://localhost&lt;/code&gt; (&lt;code&gt;python -m http.server&lt;/code&gt;) rather than as a local file, since extensions need an extra opt-in permission to run on &lt;code&gt;file://&lt;/code&gt; pages.&lt;/p&gt;

&lt;p&gt;Two honest caveats about this approach. First: I wish I'd made each button label its own scenario in the popup and the log. I ended up with a list of decoded transactions and no way to tell which button produced which, and I had to work it out from timestamps. Second, and more important: &lt;strong&gt;the harness proves the pipeline, not wallet compatibility.&lt;/strong&gt; A fake provider can't reproduce the quirks of a real one: multiple wallets fighting over &lt;code&gt;window.ethereum&lt;/code&gt;, providers that freeze their own methods, injection timing that differs per wallet. I still owe TxnLense a real test on a modern browser with real wallets before I'd claim it works with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three gotchas that each cost me an hour
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Extension context invalidated."&lt;/strong&gt; After I edited a stylesheet and reloaded the extension, every click on the test page threw this error from &lt;code&gt;content.js&lt;/code&gt;. Nothing was wrong with the code. Reloading an extension gives &lt;em&gt;new&lt;/em&gt; pages a fresh extension context, but a tab that was already open keeps running the &lt;em&gt;old&lt;/em&gt; content script, which now holds a dead connection to a service worker that no longer exists. Refresh the tab after every reload. It's a one-keystroke fix that looks like a catastrophic regression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The popup won't open itself.&lt;/strong&gt; I kept waiting for a popup to appear when I triggered a transaction. In Manifest V3 it never will: an extension's popup opens only when the user clicks its toolbar icon, and there's no API to force it. That has a design consequence. Everything the interceptor sees has to be &lt;em&gt;stored&lt;/em&gt; so the popup can read it whenever it's opened, because the popup isn't there when the interesting thing happens. It also suggests an obvious next feature: a badge on the toolbar icon when something risky goes by, since a badge is the one thing an extension is allowed to do unprompted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs live where the code lives.&lt;/strong&gt; Page-world code logs to the page's console; isolated-world code logs to the page's console under the extension's context; the service worker has its own separate DevTools. I lost real time reading the wrong console. When something is silent, check that you're looking at the right room.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Part 1 leaves out
&lt;/h2&gt;

&lt;p&gt;At the end of all this, the pipe works. A call goes in through a fake wallet, crosses two boundaries, gets decoded in a service worker, and lands in a popup, on a browser old enough that half the modern extension platform doesn't exist.&lt;/p&gt;

&lt;p&gt;But "the pipe works" and "the verdict is right" are different claims, and I'd only proven the first. The first decoded result I actually looked at was the unlimited approval, the single most famous wallet-drainer pattern there is, and the one scenario TxnLense exists to catch. The popup opened. It decoded the function correctly. It displayed the spender. It displayed the amount as &lt;code&gt;MAX (unlimited)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And across the top, in reassuring type, it said: &lt;strong&gt;LOOKS SAFE.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's Part 2.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>javascript</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>Escaping the Event Loop — A Deep Dive into worker_threads (Part 3/3)</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Tue, 04 Aug 2026 23:25:00 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/escaping-the-event-loop-a-deep-dive-into-workerthreads-part-33-11oj</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/escaping-the-event-loop-a-deep-dive-into-workerthreads-part-33-11oj</guid>
      <description>&lt;p&gt;In part 1, we built the core stack/microtask/macrotask model. In part 2, we saw how the browser and Node implement that model differently — rendering interleaved with tasks in the browser, libuv's phase-based loop in Node.&lt;/p&gt;

&lt;p&gt;Both of those posts share an unspoken assumption: your callbacks are fast. A few milliseconds, do their thing, return, let the loop move on. But what happens when that assumption breaks? What happens when you genuinely need to do slow, CPU-bound work — and there's no I/O to wait on, no callback to defer, just raw computation that has to happen?&lt;/p&gt;

&lt;p&gt;That's what this post is about. No amount of clever queue ordering saves you here. You need actual parallelism, and in Node, that means &lt;code&gt;worker_threads&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem worker_threads solves
&lt;/h2&gt;

&lt;p&gt;Everything from parts 1 and 2 works because the &lt;em&gt;actual JS execution&lt;/em&gt; — the code inside your callbacks — is assumed to be short. The event loop is brilliant at making single-threaded JS &lt;em&gt;feel&lt;/em&gt; concurrent when the bottleneck is I/O: waiting on a database, a file read, a network response. While you wait, the thread is free to do other things.&lt;/p&gt;

&lt;p&gt;But CPU-bound work is a different beast entirely. Consider this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// this takes a few seconds&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Result: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit &lt;code&gt;/fib&lt;/code&gt;, and for however long that synchronous computation runs, &lt;strong&gt;the entire server is frozen.&lt;/strong&gt; Not just that request — every request. No other client's &lt;code&gt;/OK&lt;/code&gt; request gets served, no timers fire, no I/O callbacks run, because the single JS thread is stuck inside &lt;code&gt;fibonacci(40)&lt;/code&gt; and the event loop can't do anything until the call stack unwinds.&lt;/p&gt;

&lt;p&gt;This is the single most common misunderstanding about Node's concurrency model: async I/O doesn't make your CPU-bound code non-blocking. &lt;code&gt;fs.readFile&lt;/code&gt; doesn't block because the actual disk read happens off the JS thread, in libuv's thread pool, and only the (fast) callback runs on the JS thread. But a tight synchronous loop, a big JSON parse, a hash computation, image processing — there's no I/O to hand off. It's just your code, doing math, on the one thread you've got.&lt;/p&gt;

&lt;p&gt;You have three tools in Node for genuinely parallel work, and knowing which one to reach for actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  worker_threads vs child_process vs cluster
&lt;/h2&gt;

&lt;p&gt;These get confused constantly because they all sound like "run more stuff at once." They solve different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;child_process&lt;/code&gt;&lt;/strong&gt; spawns an entirely separate OS process, potentially running a different program altogether (a shell command, a Python script, another Node script). Full isolation — separate memory space, separate V8 instance. Good for running external programs or when you want maximum isolation (a crash in the child can't take down the parent). Heavier to spin up, and communication happens via serialized IPC (stdin/stdout/message passing) — no shared memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cluster&lt;/code&gt;&lt;/strong&gt; is built on top of &lt;code&gt;child_process&lt;/code&gt;, specifically for scaling &lt;em&gt;Node servers&lt;/em&gt; across multiple CPU cores. It forks multiple copies of your entire Node process, and the OS (or a Node-managed load balancer) distributes incoming connections across them. Great for "I have a web server and want to use all my CPU cores" — but each worker is a full separate process with its own memory, event loop, everything. It's about scaling &lt;strong&gt;throughput of independent requests&lt;/strong&gt;, not about parallelizing a single computation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;worker_threads&lt;/code&gt;&lt;/strong&gt; spawns actual OS-level &lt;em&gt;threads&lt;/em&gt; within the same process — lighter weight than a full process, and critically, capable of &lt;strong&gt;sharing memory&lt;/strong&gt; via &lt;code&gt;SharedArrayBuffer&lt;/code&gt;. Each worker gets its own V8 instance and its own event loop, so it's still not shared-everything like threads in languages such as Java or C++, but message passing is fast and structured data doesn't need full IPC serialization to a separate process. This is the right tool when you have &lt;strong&gt;one chunk of CPU-heavy work you want to offload without blocking the main thread&lt;/strong&gt; — image processing, cryptographic hashing, parsing a huge file, running a compute-heavy algorithm.&lt;/p&gt;

&lt;p&gt;Quick way to decide: scaling a whole server across cores → &lt;code&gt;cluster&lt;/code&gt;. Running an external program or needing hard process isolation → &lt;code&gt;child_process&lt;/code&gt;. Offloading one CPU-bound task from inside your existing app → &lt;code&gt;worker_threads&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rest of this post is about that last one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a worker
&lt;/h2&gt;

&lt;p&gt;A worker is created from the main thread by pointing at a separate JS file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Got result:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Worker error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Worker stopped with exit code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parentPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// do some CPU-heavy work here&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heavyComputation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few key pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;parentPort&lt;/code&gt;&lt;/strong&gt; — the worker's side of the communication channel back to whoever created it. &lt;code&gt;.postMessage()&lt;/code&gt; sends data out; listening on &lt;code&gt;parentPort.on('message', ...)&lt;/code&gt; receives data in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;workerData&lt;/code&gt;&lt;/strong&gt; — lets you pass initial data into the worker at creation time, instead of messaging it in after startup:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Message passing between main thread and worker uses the &lt;strong&gt;structured clone algorithm&lt;/strong&gt; — the same mechanism &lt;code&gt;postMessage&lt;/code&gt; uses in browsers between windows/iframes. It can handle most JS values (objects, arrays, Maps, Sets, dates, even &lt;code&gt;ArrayBuffer&lt;/code&gt;s by transfer), but not everything — functions and certain non-serializable values can't cross the boundary. Each message is cloned by default, which has a real memory/CPU cost for large payloads — that's part of why &lt;code&gt;SharedArrayBuffer&lt;/code&gt; exists, more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the frozen server
&lt;/h2&gt;

&lt;p&gt;Let's rewrite the earlier example properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runFibWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./fib-worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Worker exited with code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runFibWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Result: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fib-worker.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now hitting &lt;code&gt;/fib&lt;/code&gt; spins up a worker thread to do the computation, and the main thread's event loop stays completely free to keep serving other requests while it waits for the &lt;code&gt;message&lt;/code&gt; event. This is the actual fix — not a clever async trick, but real parallel execution on a separate thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't spin up a worker per request — use a pool
&lt;/h2&gt;

&lt;p&gt;The example above works, but spawning a brand-new &lt;code&gt;Worker&lt;/code&gt; (and its own V8 instance) for every single request is expensive — worker startup isn't free. In any real system, you want a &lt;strong&gt;worker pool&lt;/strong&gt;: a fixed set of long-lived workers that pick up jobs from a queue, so you pay the startup cost once and reuse threads across many tasks.&lt;/p&gt;

&lt;p&gt;Here's a reasonably complete pool implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker-pool.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WorkerPool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workerScript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpus&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workerScript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeWorkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_addWorker&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="nf"&gt;_addWorker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerScript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_takeNextTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTask&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Replace the crashed worker so the pool stays at full size&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_addWorker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeWorkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;_takeNextTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worker&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeWorkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;runTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;freeWorker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeWorkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;freeWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;freeWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nx"&gt;freeWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WorkerPool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pool-worker.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parentPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WorkerPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker-pool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WorkerPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./pool-worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Result: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sizing the pool to &lt;code&gt;os.cpus().length&lt;/code&gt; is a sensible default — one worker per logical core, so you're not oversubscribing the CPU with more compute-bound threads than you have cores to run them on. This pattern (or a maintained library like &lt;code&gt;piscina&lt;/code&gt;, which does essentially this with more polish) is genuinely how you'd offload CPU work in production Node.&lt;/p&gt;

&lt;h2&gt;
  
  
  SharedArrayBuffer and Atomics — when message passing isn't enough
&lt;/h2&gt;

&lt;p&gt;By default, data sent between the main thread and a worker is &lt;strong&gt;cloned&lt;/strong&gt;. For most use cases that's fine, but for large datasets — a big typed array you want multiple workers reading and writing without copying gigabytes back and forth — cloning becomes the bottleneck itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SharedArrayBuffer&lt;/code&gt; gives you a chunk of memory that's genuinely shared between threads — no cloning, both sides see the same bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharedBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SharedArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 bytes = one Int32&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharedArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Int32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sharedBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./increment-worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sharedBuffer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Final value:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sharedArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// both threads touched the same memory&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// increment-worker.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentPort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharedArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Int32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sharedBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;Atomics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sharedArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the use of &lt;code&gt;Atomics.add&lt;/code&gt; rather than &lt;code&gt;sharedArray[0]++&lt;/code&gt;. This matters: when multiple threads can touch the same memory simultaneously, plain read-modify-write operations are subject to race conditions — two threads can both read the same old value before either writes back, and one increment gets silently lost. &lt;code&gt;Atomics&lt;/code&gt; provides operations (&lt;code&gt;add&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;compareExchange&lt;/code&gt;, &lt;code&gt;load&lt;/code&gt;, &lt;code&gt;store&lt;/code&gt;, and more) that are guaranteed to be indivisible at the hardware level, so concurrent access doesn't corrupt the value. &lt;code&gt;Atomics.wait&lt;/code&gt; and &lt;code&gt;Atomics.notify&lt;/code&gt; go further, letting threads actually block and signal each other — a real synchronization primitive, not just safe arithmetic.&lt;/p&gt;

&lt;p&gt;This is a genuinely deep topic on its own — safe concurrent memory access is one of the harder problems in any multithreaded system, not a JS-specific quirk — so treat this section as "know it exists and roughly what it's for," not a complete guide. Most real workloads are well served by message passing through a pool; reach for &lt;code&gt;SharedArrayBuffer&lt;/code&gt; specifically when you've profiled and confirmed that cloning large data is the actual bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to actually reach for worker_threads
&lt;/h2&gt;

&lt;p&gt;Worth being honest about the tradeoffs before you reach for this in your own code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Worker startup has real cost.&lt;/strong&gt; Spinning up a V8 instance per thread isn't free — this is exactly why pooling matters for anything beyond a one-off script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not everything benefits.&lt;/strong&gt; If your bottleneck is I/O (database calls, network requests, file reads), workers don't help — that's exactly what the async event loop from parts 1 and 2 already handles well, without the overhead of a thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message passing has a cost too.&lt;/strong&gt; For small payloads it's negligible; for large ones, either restructure to send less data or use &lt;code&gt;SharedArrayBuffer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging is harder.&lt;/strong&gt; Errors in a worker don't naturally propagate the way synchronous exceptions do — you have to explicitly listen for &lt;code&gt;'error'&lt;/code&gt; events, and stack traces across threads are less convenient to work with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest heuristic: profile first. If your server is slow because it's waiting on a database, adding worker threads won't fix anything — that's an I/O problem, and the event loop already handles it well. Worker threads earn their complexity specifically when you've identified genuine CPU-bound work — a synchronous computation heavy enough to visibly block the main thread — and optimizing the algorithm itself isn't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tying the series together
&lt;/h2&gt;

&lt;p&gt;Three parts, one throughline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1&lt;/strong&gt; gave you the spec-level model — stack, microtask queue, macrotask queue, and the rule that microtasks always drain first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt; showed that model isn't implemented identically everywhere — the browser interleaves it with rendering, Node runs it through libuv's ordered phases, and Node adds &lt;code&gt;process.nextTick()&lt;/code&gt; as a queue-jumper the browser doesn't have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt; showed the model's actual limit — a single thread, no matter how cleverly you schedule callbacks on it, can't parallelize genuine CPU-bound work. &lt;code&gt;worker_threads&lt;/code&gt; is Node's way out of that constraint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together, this is really the full picture of "how does async JS work" — from the queue ordering that decides what runs next, to the runtime-specific machinery underneath it, to the point where you need real threads instead of clever scheduling. If you made it through all three, you're in a genuinely small group of JS developers who could actually explain &lt;em&gt;why&lt;/em&gt; &lt;code&gt;setImmediate&lt;/code&gt; and &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; race unpredictably at the top level of a script, or &lt;em&gt;why&lt;/em&gt; a recursive &lt;code&gt;.then()&lt;/code&gt; chain can freeze a browser tab without ever blocking the call stack.&lt;/p&gt;

&lt;p&gt;That's the kind of understanding that turns "the event loop is confusing" into "the event loop is just a queue, some phases, and a very specific set of rules about what runs first" — which, at the end of the day, it is.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Browser vs Node — Where the Event Loop Actually Diverges (Part 2/3)</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:21:11 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/browser-vs-node-where-the-event-loop-actually-diverges-part-23-3j6c</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/browser-vs-node-where-the-event-loop-actually-diverges-part-23-3j6c</guid>
      <description>&lt;p&gt;In part 1, we built the shared mental model: call stack, microtask queue, macrotask queue, and the rule that microtasks fully drain before the next macrotask runs. That model is spec-level JavaScript behavior — but it's not the whole story once you actually run code.&lt;/p&gt;

&lt;p&gt;The event loop isn't part of the JS language spec. It's part of the &lt;strong&gt;host environment&lt;/strong&gt; — the browser or Node — and each one implements it differently around that shared core. This is the post most "event loop" explainers skip, because it means going past the diagram and into how each runtime is actually built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser: event loop meets rendering
&lt;/h2&gt;

&lt;p&gt;In a browser, the event loop isn't just juggling callbacks — it's also responsible for keeping the page visually responsive. That means rendering has to get a turn too, and the browser has to decide &lt;em&gt;when&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's the roughly accurate sequence per loop iteration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Execute one macrotask (a click handler, a &lt;code&gt;setTimeout&lt;/code&gt; callback, a network event, whatever's next in the queue)&lt;/li&gt;
&lt;li&gt;Drain the entire microtask queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maybe&lt;/strong&gt; render a frame — the browser doesn't render after every single task; it tries to hit ~60fps and will batch work between paints&lt;/li&gt;
&lt;li&gt;Go back to step 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "maybe render" part is where two APIs come in that don't exist in Node at all:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requestAnimationFrame(callback)&lt;/code&gt;&lt;/strong&gt; — schedules a callback to run right before the next repaint. It's not a macrotask or microtask in the queue sense — it's tied directly to the rendering pipeline. Use it for anything visual (animations, DOM measurements) instead of &lt;code&gt;setTimeout&lt;/code&gt;, because it's synced to when the browser is actually about to paint, not an arbitrary delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requestIdleCallback(callback)&lt;/code&gt;&lt;/strong&gt; — schedules a callback to run when the browser is idle, after layout and paint, with a deadline. Meant for low-priority work you don't want competing with rendering — analytics, prefetching, non-urgent DOM updates.&lt;/p&gt;

&lt;p&gt;Here's the key interaction that's easy to miss: &lt;strong&gt;microtasks can starve rendering.&lt;/strong&gt; If a promise chain keeps queueing more microtasks, the browser can't get to the paint step, because microtasks always drain fully before rendering gets a turn. This is a real, debuggable performance bug — a runaway &lt;code&gt;.then()&lt;/code&gt; chain can visibly freeze a page even though "nothing is blocking the main thread" in the traditional sync-loop sense.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recursiveMicrotask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recursiveMicrotask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;recursiveMicrotask&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Page becomes unresponsive — not because the stack is blocked,&lt;/span&gt;
&lt;span class="c1"&gt;// but because the microtask queue never empties long enough for a paint.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to a &lt;code&gt;setTimeout&lt;/code&gt;-based recursive loop — because each iteration is a separate macrotask, the browser gets a chance to render between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node: no rendering, but a much more structured loop
&lt;/h2&gt;

&lt;p&gt;Node doesn't render anything, so it doesn't need the "maybe paint" logic. Instead, it's built on &lt;strong&gt;libuv&lt;/strong&gt;, a C library that gives Node its event loop, thread pool, and async I/O. libuv organizes the loop into distinct &lt;strong&gt;phases&lt;/strong&gt;, each with its own FIFO queue of callbacks. This is a meaningfully different shape from the browser's single task queue.&lt;/p&gt;

&lt;p&gt;The phases, in order, each loop tick:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;timers&lt;/strong&gt; — runs callbacks scheduled by &lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; whose threshold has elapsed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pending callbacks&lt;/strong&gt; — executes I/O callbacks deferred from the previous loop iteration (some system-level TCP errors, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;idle, prepare&lt;/strong&gt; — internal use only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;poll&lt;/strong&gt; — the big one: retrieves new I/O events, executes I/O-related callbacks (almost everything — file reads, network requests). Node will block here waiting for new events if there's nothing else scheduled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check&lt;/strong&gt; — &lt;code&gt;setImmediate()&lt;/code&gt; callbacks run here, specifically &lt;em&gt;after&lt;/em&gt; poll&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;close callbacks&lt;/strong&gt; — e.g. &lt;code&gt;socket.on('close', ...)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then it loops back to timers.&lt;/p&gt;

&lt;p&gt;Between &lt;strong&gt;every single callback&lt;/strong&gt; — not just between phases, but between individual callbacks within a phase — Node drains the microtask queue. Same drain-fully rule as the browser, just applied at a finer grain because there's no rendering to interleave with.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;process.nextTick()&lt;/code&gt; — Node's queue-jumper
&lt;/h2&gt;

&lt;p&gt;Node has a queue that doesn't exist in the browser at all: &lt;code&gt;process.nextTick()&lt;/code&gt;. Despite the name, it doesn't queue for "next tick" of the event loop — it runs &lt;strong&gt;before microtasks&lt;/strong&gt;, after the current operation finishes, no matter what.&lt;/p&gt;

&lt;p&gt;Priority order in Node, after any synchronous code completes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;process.nextTick()&lt;/code&gt; queue (fully drained)&lt;/li&gt;
&lt;li&gt;Promise microtask queue (fully drained)&lt;/li&gt;
&lt;li&gt;Next macrotask/phase callback
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nextTick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;sync&lt;/code&gt;, &lt;code&gt;nextTick&lt;/code&gt;, &lt;code&gt;promise&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nextTick&lt;/code&gt; beats the promise every time, because Node checks and drains the &lt;code&gt;nextTick&lt;/code&gt; queue first, and — same recursion risk as the microtask-starvation example above — a recursive &lt;code&gt;process.nextTick()&lt;/code&gt; call can starve I/O entirely, since Node won't proceed past it to the poll phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;setImmediate()&lt;/code&gt; vs &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; — the ambiguous one
&lt;/h2&gt;

&lt;p&gt;This is the example that shows up in almost every Node interview, and the honest answer is: &lt;strong&gt;it depends on where you call it from.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;setImmediate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;immediate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this at the &lt;strong&gt;top level&lt;/strong&gt; of a script, and the order is not guaranteed — it depends on process startup timing, specifically whether the timers phase's threshold has already elapsed by the time the loop starts. You'll see it flip between runs.&lt;/p&gt;

&lt;p&gt;But inside an I/O callback, the order is deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setImmediate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;immediate&lt;/span&gt;&lt;span class="dl"&gt;'&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;p&gt;Output is always &lt;code&gt;immediate&lt;/code&gt;, then &lt;code&gt;timeout&lt;/code&gt;. Why? Because &lt;code&gt;fs.readFile&lt;/code&gt;'s callback runs in the &lt;strong&gt;poll&lt;/strong&gt; phase. From there, the loop moves to &lt;strong&gt;check&lt;/strong&gt; next — where &lt;code&gt;setImmediate&lt;/code&gt; lives — before it wraps back around to &lt;strong&gt;timers&lt;/strong&gt;. The phase order guarantees it here, where at the top level there's no such guarantee.&lt;/p&gt;

&lt;p&gt;This is a genuinely useful thing to internalize: &lt;code&gt;setImmediate&lt;/code&gt; means "run in the check phase, this loop iteration," while &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; means "run in the timers phase, next time the loop gets there" — and those are different guarantees depending on what phase you're currently in when you schedule them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side: browser vs Node
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Underlying engine&lt;/td&gt;
&lt;td&gt;V8 (Chrome), SpiderMonkey (Firefox), etc.&lt;/td&gt;
&lt;td&gt;V8 + libuv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue structure&lt;/td&gt;
&lt;td&gt;Single task queue + microtask queue&lt;/td&gt;
&lt;td&gt;Multiple phase-specific queues (libuv) + microtask queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering concern&lt;/td&gt;
&lt;td&gt;Yes — interleaved between tasks&lt;/td&gt;
&lt;td&gt;No rendering at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setTimeout(fn, 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs as next macrotask, after microtasks drain&lt;/td&gt;
&lt;td&gt;Runs in the timers phase, next time loop reaches it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setImmediate()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Doesn't exist&lt;/td&gt;
&lt;td&gt;Runs in the check phase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;process.nextTick()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Doesn't exist&lt;/td&gt;
&lt;td&gt;Runs before microtasks, after current operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle/low-priority work&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requestIdleCallback()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No direct equivalent — usually just &lt;code&gt;setImmediate()&lt;/code&gt; or offloading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animation timing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requestAnimationFrame()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No equivalent — no rendering to sync to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I/O model&lt;/td&gt;
&lt;td&gt;Web APIs (fetch, XHR, DOM events)&lt;/td&gt;
&lt;td&gt;libuv (thread pool for fs, async for network)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What carries over, and what doesn't
&lt;/h2&gt;

&lt;p&gt;The stack/microtask/macrotask model from part 1 is real in both environments — that part's spec-driven and doesn't change. But the &lt;em&gt;shape&lt;/em&gt; of the macrotask side is genuinely different: the browser has one queue and a rendering step to juggle; Node has an ordered set of phases with distinct semantics per phase, plus &lt;code&gt;process.nextTick()&lt;/code&gt; sitting in front of everything.&lt;/p&gt;

&lt;p&gt;If you've been assuming "the event loop" is one universal thing you can reason about the same way in a React component and an Express handler, this is usually where that assumption breaks — and where subtle bugs (starved I/O from recursive &lt;code&gt;nextTick&lt;/code&gt;, or a &lt;code&gt;setTimeout&lt;/code&gt;-vs-&lt;code&gt;setImmediate&lt;/code&gt; race at startup) actually come from in production code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming up next
&lt;/h2&gt;

&lt;p&gt;Both of these models assume your callbacks are fast. But what happens when you genuinely have CPU-heavy work — image processing, large computations, parsing — that can't be broken into tiny async chunks? No amount of clever queue ordering saves you if a single synchronous function blocks the thread for 500ms.&lt;/p&gt;

&lt;p&gt;Part 3 covers Node's answer to that: &lt;code&gt;worker_threads&lt;/code&gt; — real OS-level threads, how to actually use them, and when reaching for a worker beats just optimizing your algorithm.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>The JS Event Loop — Core Mental Model (Part 1/3)</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:13:49 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/the-js-event-loop-core-mental-model-part-13-289i</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/the-js-event-loop-core-mental-model-part-13-289i</guid>
      <description>&lt;p&gt;JavaScript is single-threaded. One call stack, one thing happening at a time. And yet your code fetches data, sets timers, listens for clicks, and handles I/O — all without blocking. How?&lt;/p&gt;

&lt;p&gt;The answer is the &lt;strong&gt;event loop&lt;/strong&gt;, and it's one of those concepts every JS developer &lt;em&gt;thinks&lt;/em&gt; they understand until someone asks them to predict the output of five nested &lt;code&gt;setTimeout&lt;/code&gt; and &lt;code&gt;Promise&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;This is part 1 of a 3-part series:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The core event loop model&lt;/strong&gt; (this post)&lt;/li&gt;
&lt;li&gt;Where Node and the browser actually diverge&lt;/li&gt;
&lt;li&gt;Escaping the event loop with &lt;code&gt;worker_threads&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's build the mental model properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  JS is single-threaded — so what's actually running your async code?
&lt;/h2&gt;

&lt;p&gt;The JS engine (V8, SpiderMonkey, etc.) only executes one thing at a time on the &lt;strong&gt;call stack&lt;/strong&gt;. There's no multithreading inside the engine itself.&lt;/p&gt;

&lt;p&gt;But the &lt;em&gt;runtime&lt;/em&gt; around the engine — the browser or Node — provides extra machinery: Web APIs in the browser (timers, DOM events, fetch), or C++ APIs via libuv in Node (timers, file I/O, network). These run outside the JS thread. When they finish, they don't just barge into your running code — they queue up a callback to be run later.&lt;/p&gt;

&lt;p&gt;That queueing and "run later" part is the event loop's job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three pieces: stack, queue, loop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Call stack&lt;/strong&gt; — where synchronous code executes, frame by frame. If a function calls another function, it stacks. When a function returns, it pops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue(s)&lt;/strong&gt; — where callbacks wait after some async operation completes. There isn't just one queue (more on that in a second).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event loop&lt;/strong&gt; — a simple, repeating check: &lt;em&gt;"Is the call stack empty? If yes, take the next thing from a queue and push it onto the stack."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's genuinely most of it. The complexity comes from &lt;strong&gt;which queue goes first&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Macrotasks vs microtasks
&lt;/h2&gt;

&lt;p&gt;This is the part that trips people up, so let's be precise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Macrotasks&lt;/strong&gt; (aka "tasks") include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; callbacks&lt;/li&gt;
&lt;li&gt;I/O callbacks&lt;/li&gt;
&lt;li&gt;UI rendering-related callbacks (browser)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Microtasks&lt;/strong&gt; include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Promise.then&lt;/code&gt; / &lt;code&gt;.catch&lt;/code&gt; / &lt;code&gt;.finally&lt;/code&gt; callbacks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queueMicrotask()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;async/await&lt;/code&gt; continuations (they're sugar over promises)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule that matters: &lt;strong&gt;after every single macrotask, the event loop fully drains the microtask queue before it does anything else&lt;/strong&gt; — including before rendering a frame or running the next macrotask.&lt;/p&gt;

&lt;p&gt;Not "checks it once." Drains it completely. If a microtask queues another microtask, that runs too, before moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The classic gotcha, explained properly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to predict the order before reading on.&lt;/p&gt;

&lt;p&gt;Here's what happens, step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;console.log('1')&lt;/code&gt; runs synchronously → prints &lt;code&gt;1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setTimeout(...)&lt;/code&gt; hands its callback off to the runtime's timer API and registers a &lt;strong&gt;macrotask&lt;/strong&gt; for later — even with a &lt;code&gt;0ms&lt;/code&gt; delay, it doesn't run immediately&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Promise.resolve().then(...)&lt;/code&gt; queues a &lt;strong&gt;microtask&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;console.log('4')&lt;/code&gt; runs synchronously → prints &lt;code&gt;4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Call stack is now empty. Event loop checks the &lt;strong&gt;microtask queue first&lt;/strong&gt; → runs it → prints &lt;code&gt;3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Microtask queue is empty. Event loop moves to the &lt;strong&gt;macrotask queue&lt;/strong&gt; → runs the timeout callback → prints &lt;code&gt;2&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Output: &lt;code&gt;1, 4, 3, 2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The part people get wrong is assuming &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; means "run next." It doesn't mean "run next" — it means "run next &lt;em&gt;macrotask&lt;/em&gt; turn," and microtasks always cut the line first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this ordering exists at all
&lt;/h2&gt;

&lt;p&gt;It's not arbitrary. Microtasks are meant for &lt;strong&gt;finishing up work that's already in flight&lt;/strong&gt; — resolving a promise chain, reacting to something that just happened — before the system moves on to genuinely new, separately-scheduled work like a timer firing or an I/O event arriving. It keeps related async logic coherent and predictable, rather than letting it get interleaved with unrelated queued tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick check — predict the output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promise 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promise 2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop your answer in the comments before scrolling through devtools to check — this is exactly the kind of ordering that gets asked in interviews, and exactly the kind of bug that shows up in real async code when you assume timers run "immediately."&lt;/p&gt;

&lt;p&gt;Answer&lt;/p&gt;

&lt;p&gt;&lt;code&gt;start&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;, &lt;code&gt;promise 1&lt;/code&gt;, &lt;code&gt;promise 2&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;.then()&lt;/code&gt; callbacks are microtasks and both drain before the macrotask queue is touched — even though the second &lt;code&gt;.then()&lt;/code&gt; is only &lt;em&gt;queued&lt;/em&gt; once the first one runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This model — stack, microtask queue, macrotask queue, drain-before-next-task — is the &lt;strong&gt;shared spec-level behavior&lt;/strong&gt; both browsers and Node implement. But they don't implement the &lt;em&gt;runtime&lt;/em&gt; around it the same way.&lt;/p&gt;

&lt;p&gt;Browsers have to interleave this with rendering. Node runs on libuv with distinct phases (timers, I/O, &lt;code&gt;setImmediate&lt;/code&gt;, close callbacks) and has its own microtask-like queue via &lt;code&gt;process.nextTick()&lt;/code&gt; that jumps the line even ahead of promises.&lt;/p&gt;

&lt;p&gt;That's where things get genuinely different — and where most "event loop" explanations stop short. Part 2 goes there.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Before Solana Went Native: What the EVM Actually Is (And Why It Was Built to Be Slow)</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Mon, 13 Jul 2026 18:04:17 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/before-solana-went-native-what-the-evm-actually-is-and-why-it-was-built-to-be-slow-14h7</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/before-solana-went-native-what-the-evm-actually-is-and-why-it-was-built-to-be-slow-14h7</guid>
      <description>&lt;p&gt;To understand why Solana's decision to repurpose BPF was radical, you first need to understand what it was radical &lt;em&gt;against&lt;/em&gt;. That's the Ethereum Virtual Machine — the thing every other "VM for smart contracts" design, Solana included, is implicitly arguing with.&lt;/p&gt;

&lt;p&gt;The EVM isn't slow because Ethereum's engineers were careless. It's slow because of a set of design choices that made total sense in 2014, when the problem being solved wasn't "how do we get 50,000 TPS" — it was "how do we get a global network of mutually distrusting strangers to agree on the exact same computation, byte for byte, forever." Every property that makes the EVM feel heavy today is downstream of that one requirement.&lt;/p&gt;

&lt;p&gt;This is the prequel to a piece I wrote on how Solana re-engineered a Linux kernel packet filter into its execution layer. Before we get to what Solana did differently, here's what it was different &lt;em&gt;from&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. A Stack Machine, Not a Register Machine
&lt;/h2&gt;

&lt;p&gt;The EVM is a &lt;strong&gt;256-bit stack-based virtual machine&lt;/strong&gt;. There are no general-purpose registers. Every operation — arithmetic, comparisons, memory access — pushes and pops values from a stack, one instruction at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nasm"&gt;&lt;code&gt;&lt;span class="nf"&gt;PUSH1&lt;/span&gt; &lt;span class="mh"&gt;0x05&lt;/span&gt;
&lt;span class="nf"&gt;PUSH1&lt;/span&gt; &lt;span class="mh"&gt;0x03&lt;/span&gt;
&lt;span class="nf"&gt;ADD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire bytecode for "5 + 3." No register allocation, no scheduling — the EVM interprets it top to bottom, exactly as written. This is also why Solidity compiles down to something conceptually close to this, opcode by opcode, rather than to a register-mapped instruction set the way a Rust program compiling to BPF does.&lt;/p&gt;

&lt;p&gt;The 256-bit word size isn't an accident either — it matches the output width of Keccak-256, the hash function baked into almost everything Ethereum does (addresses, storage slots, opcodes like &lt;code&gt;SHA3&lt;/code&gt;). The VM's fundamental data type was chosen to fit the cryptography, not the hardware.&lt;/p&gt;

&lt;p&gt;Compare that to a register machine like BPF or SBF, where the instruction set is designed to map closely to what a real CPU already does. The EVM was never trying to be fast on real silicon. It was trying to be &lt;em&gt;unambiguous&lt;/em&gt; — every implementation, on every machine, computing the exact same stack transitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Interpreted, All the Way Down
&lt;/h2&gt;

&lt;p&gt;There's no JIT. No AOT compilation to native code. Every EVM opcode, on every node, for every transaction, gets interpreted at runtime, instruction by instruction, by whatever client software that node happens to run (Geth, Nethermind, Besu — doesn't matter, they all have to agree).&lt;/p&gt;

&lt;p&gt;This is a deliberate constraint, not an oversight. If clients were free to JIT-compile bytecode to native machine code, you'd be trusting each client's compiler to preserve &lt;em&gt;exact&lt;/em&gt; semantics across compilation — including edge cases like integer overflow behavior and gas accounting quirks. One divergent optimization and the network forks. Interpretation is slow, but it's slow in a way that's trivially auditable: the reference behavior &lt;em&gt;is&lt;/em&gt; the implementation.&lt;/p&gt;

&lt;p&gt;Some newer EVM implementations do experiment with JIT compilation for performance, but the base protocol was never designed assuming it — correctness came first, speed was whatever was left over.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Gas: Metering Before Execution, Not Verification Before Execution
&lt;/h2&gt;

&lt;p&gt;The EVM has no static verifier checking that your bytecode terminates before it runs — there's no equivalent of eBPF's compile-time proof-of-termination. Instead, every single opcode has a fixed gas cost, charged as execution proceeds. Run out of gas, execution halts immediately and any state changes revert.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function withdraw(uint amount) public {
    require(amount &amp;lt;= balances[msg.sender], "insufficient balance");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line here costs gas — &lt;code&gt;SLOAD&lt;/code&gt; to read &lt;code&gt;balances[msg.sender]&lt;/code&gt;, &lt;code&gt;SSTORE&lt;/code&gt; to write it back, &lt;code&gt;CALL&lt;/code&gt; to send funds. The sender pre-pays an upper bound before the transaction even starts, and unused gas gets refunded. This is a fundamentally different safety model than a verifier: instead of proving a program &lt;em&gt;can't&lt;/em&gt; misbehave, you make misbehaving economically self-limiting. An infinite loop doesn't hang the network — it just burns through the caller's gas and reverts, at their expense.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Account-Based State and Why Everything Runs in Order
&lt;/h2&gt;

&lt;p&gt;Ethereum's state is a giant key-value mapping — accounts to balances, contract storage slots to values — represented as a Merkle Patricia Trie so any node can cryptographically prove the current state root. Crucially, a transaction doesn't declare up front which storage slots it's going to touch. It just runs, and reads/writes whatever it wants as it goes, including calling into other contracts that touch state you couldn't have predicted ahead of time.&lt;/p&gt;

&lt;p&gt;That's the detail that quietly determines Ethereum's entire concurrency story. If you don't know in advance what a transaction will touch, you can't safely run two transactions at once without risking a race on shared state. So the EVM, as a base protocol, executes transactions &lt;strong&gt;strictly sequentially&lt;/strong&gt;, one at a time, in block order. Parallel execution research exists (both inside and outside core Ethereum client teams), but it's an optimization bolted on after the fact, not a property the VM was built around.&lt;/p&gt;

&lt;p&gt;This is precisely the constraint Solana's Sealevel runtime sidesteps — by &lt;em&gt;requiring&lt;/em&gt; transactions to declare their account access lists upfront, Solana's runtime can prove non-overlap before execution and schedule accordingly. That's not a coincidence; it's a direct response to this exact bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It's Built This Way
&lt;/h2&gt;

&lt;p&gt;None of this is Ethereum "doing it wrong." A stack machine with no JIT, gas-metered instead of statically verified, sequential by default — every one of these was the correct trade-off for a network whose founding problem was trustless consensus among strangers, not raw throughput. Determinism and auditability were the product. Speed was never the spec.&lt;/p&gt;

&lt;p&gt;That's exactly the gap Solana's architects looked at and decided to close from a completely different direction — not by making a better stack machine, but by throwing the stack-machine model out and grabbing a register-based, hardware-native VM that Linux had already spent two decades optimizing. What that actually involved — stripping the kernel verifier, zero-copy account access, AOT compilation to native code, and the account-declaration trick that unlocks parallelism — is the subject of the piece this one precedes.&lt;/p&gt;

&lt;p&gt;If you haven't read it yet: &lt;a href="https://dev.to/aniket_misra_e47d1564ab7b/from-packet-filter-to-high-performance-execution-layer-how-solana-re-engineered-bpf-214i"&gt;From Packet Filter to High-Performance Execution Layer: How Solana Re-Engineered BPF&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>performance</category>
    </item>
    <item>
      <title>Kill the Server: Why Holepunch Threw Away Node.js and Built 'Bare'</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Sat, 11 Jul 2026 03:07:00 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/kill-the-server-why-holepunch-threw-away-nodejs-and-built-bare-3gdi</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/kill-the-server-why-holepunch-threw-away-nodejs-and-built-bare-3gdi</guid>
      <description>&lt;p&gt;When the Holepunch team set out to build Pear—a decentralized, peer-to-peer application runtime—they started with the obvious choice: Node.js. &lt;/p&gt;

&lt;p&gt;Node is the undisputed king of JavaScript outside the browser. It has a massive ecosystem, a battle-tested asynchronous event loop (&lt;code&gt;libuv&lt;/code&gt;), and the raw execution speed of Google's V8 engine. It seemed like the perfect foundation for a P2P stack.&lt;/p&gt;

&lt;p&gt;But as they dug deeper into cross-platform routing, NAT traversal, and mobile embedding, they hit a fundamental architectural roadblock: &lt;strong&gt;Node.js makes too many assumptions, and the biggest assumption it makes is that you are running a server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Node was built for the data center. It carries decades of legacy APIs (&lt;code&gt;http&lt;/code&gt;, &lt;code&gt;net&lt;/code&gt;, &lt;code&gt;tls&lt;/code&gt;) that are tightly coupled to centralized, client-server web architecture. When you want to build a purely peer-to-peer, serverless network where devices connect directly via a Distributed Hash Table (DHT), all of that built-in Node bloat becomes dead weight.&lt;/p&gt;

&lt;p&gt;So, they did what any obsessive systems engineering team does. They stripped Node down to its studs, threw away the bloated standard library, and built &lt;strong&gt;Bare&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is a technical look at the Bare runtime, and why throwing away Node’s HTTP assumptions is the key to true P2P applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Deconstructing the Runtime: What is Bare?
&lt;/h2&gt;

&lt;p&gt;At its core, Bare is a minimalist JavaScript runtime designed specifically for desktop, mobile, and IoT embedding. &lt;/p&gt;

&lt;p&gt;It keeps the best parts of Node's foundational architecture but aggressively decouples the JavaScript engine from the system APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Traditional Node.js Stack ]        [ The Bare Runtime Stack ]
┌─────────────────────────┐          ┌─────────────────────────┐
│     User Application    │          │     User Application    │
├─────────────────────────┤          ├─────────────────────────┤
│ Node Standard Library   │          │     Userland Modules    │
│ (http, fs, net, crypto) │          │ (HyperDHT, Hyperdrive)  │
├─────────────────────────┤          ├─────────────────────────┤
│    Node C++ Bindings    │          │           Bare          │
├────────────┬────────────┤          ├────────────┬────────────┤
│     V8     │   libuv    │          │    libjs   │   libuv    │
└────────────┴────────────┘          ├────────────┴────────────┤
                                     │V8/ QuickJS / JerryScript│
                                     └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the key difference at the bottom of the stack: libjs.&lt;/p&gt;

&lt;p&gt;Node is rigidly bound to Google’s V8 engine. Bare abstracts the JavaScript engine behind a C-API wrapper called libjs. This is a massive systems-level advantage. While Bare runs V8 by default for desktop performance, libjs allows developers to swap out the engine entirely. If you are deploying a P2P application to a highly constrained LTE router or a microcontroller, you can swap V8 for lightweight engines like QuickJS or JerryScript without changing the core runtime architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Missing Standard Library (A Feature, Not a Bug)
&lt;/h3&gt;

&lt;p&gt;If you install Bare and try to spin up a quick web server, it will fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This works in Node. It fails in Bare.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="c1"&gt;// Error: Cannot find module 'http'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bare intentionally ships with almost nothing. There is no http. There is no net. There is no crypto.&lt;/p&gt;

&lt;p&gt;Why? Because in a true peer-to-peer architecture, standard HTTP is an anti-pattern. If you rely on http, you are relying on DNS routing, centralized Certificate Authorities (TLS), and exposed public IP addresses.&lt;/p&gt;

&lt;p&gt;Instead of forcing a heavy standard library into the binary, Bare leaves feature implementation entirely to userland modules. The runtime provides only three core primitives:&lt;/p&gt;

&lt;p&gt;1.A module system (with bidirectional CJS and ESM interoperability).  &lt;/p&gt;

&lt;p&gt;2.A native addon system (for linking low-level C/C++ libraries).  &lt;/p&gt;

&lt;p&gt;3.Lightweight threads (with SharedArrayBuffer support).&lt;/p&gt;

&lt;p&gt;Everything else is imported a-la-carte. When you want to build a network connection in Bare, you don't use http. You use Holepunch's hyperdht, utilizing cryptographic keys instead of IPs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.Bare-Metal Embedding (Desktop to Mobile)
&lt;/h3&gt;

&lt;p&gt;Node is notoriously difficult to embed cleanly into mobile applications. If you want to run a Node instance inside an iOS app, you end up wrestling with massive binaries, battery drain, and messy IPC (Inter-Process Communication) bridges.&lt;/p&gt;

&lt;p&gt;Because Bare shed the standard library, its memory footprint is drastically reduced. It treats mobile as a first-class citizen.  &lt;/p&gt;

&lt;p&gt;Through Bare Kit, developers can spin up "worklets"—isolated Bare threads running directly inside native mobile frameworks (SwiftUI for iOS, or Android Services).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spawning a Bare worklet for background P2P sync&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worklet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bare-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;syncThread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worklet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/hyperdrive-sync.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;syncThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mobile UI received state update from P2P swarm:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;syncThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;START_SYNC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows a React Native or Swift frontend to offload all the heavy lifting—DHT routing, UDP hole punching, and data replication—to a highly efficient, native background thread that won't freeze the mobile UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway: Stop Building Servers
&lt;/h3&gt;

&lt;p&gt;When you look at Bare, you realize that the Node.js architecture we’ve been using for 15 years has subtly brainwashed us. We assume that writing backend JavaScript inherently means writing server logic.Bare proves that JavaScript can be used for something far more resilient. By stripping away the bloated legacy of the Web2 data center, Holepunch has created a runtime that actually belongs on the edge. It forces you to stop thinking about endpoints and status codes, and start thinking about swarms, peers, and cryptographic tunnels.  If you want to build systems that governments can't block and cloud providers can't crash, you have to kill the server. Bare is the runtime designed to do exactly that.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>bare</category>
      <category>holepunch</category>
    </item>
    <item>
      <title>The Dark Art of UDP Hole Punching: How to Build a Serverless Web</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:57:35 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/the-dark-art-of-udp-hole-punching-how-to-build-a-serverless-web-1p7d</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/the-dark-art-of-udp-hole-punching-how-to-build-a-serverless-web-1p7d</guid>
      <description>&lt;p&gt;If you want to build a truly decentralized application—one that doesn't rely on AWS, Vercel, or a centralized RPC node—you immediately run into a brick wall of network physics. &lt;/p&gt;

&lt;p&gt;You want User A to talk directly to User B. &lt;br&gt;
The problem? Neither of them has a public IP address.&lt;/p&gt;

&lt;p&gt;Because we ran out of IPv4 addresses decades ago, ISPs placed almost every consumer device on earth behind a &lt;strong&gt;NAT (Network Address Translation)&lt;/strong&gt; router. NAT is a one-way mirror. You can send outbound requests to a public server, and the router will allow the response back in. But if an external device tries to initiate a connection with you, the NAT drops the packet immediately. It assumes it is hostile.&lt;/p&gt;

&lt;p&gt;This single piece of hardware killed the peer-to-peer internet. &lt;/p&gt;

&lt;p&gt;To build a sovereign, serverless application layer (like what Holepunch and Pear are doing), you have to bypass the NAT. You have to trick the router into letting inbound traffic through. This technique is called &lt;strong&gt;UDP Hole Punching&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Here is the architectural deep dive into how it works, and how modern cryptographic routing makes it scalable.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. The Mechanics of the "Hole Punch"
&lt;/h2&gt;

&lt;p&gt;Unlike TCP, which requires a rigid 3-way handshake, &lt;strong&gt;UDP is connectionless&lt;/strong&gt;. It just fires raw datagrams into the void.&lt;/p&gt;

&lt;p&gt;When your laptop sends a UDP packet to an external server, your NAT router intercepts it, temporarily maps your local IP/port to its public IP/port, and adds a record to its &lt;strong&gt;State Table&lt;/strong&gt;. For the next 30 to 60 seconds, it leaves a "hole" open. If any UDP packet hits that specific public port, the NAT assumes it is a valid response and forwards it to your laptop.&lt;/p&gt;

&lt;p&gt;If Alice and Bob are both behind strict NATs, they cannot ping each other. But if they coordinate, they can exploit the state table.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Signaling Phase:&lt;/strong&gt; Alice and Bob both connect to a third-party relay to discover their own respective public IP addresses and ports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Simultaneous Strike:&lt;/strong&gt; Alice fires a UDP packet directly at Bob's public IP. Bob fires a UDP packet directly at Alice's public IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Rejection:&lt;/strong&gt; Alice's first packet hits Bob's router and gets instantly dropped, because Bob's router hasn't opened a hole yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Breakthrough:&lt;/strong&gt; But when Alice fired that packet, &lt;em&gt;her&lt;/em&gt; router opened a hole, expecting a response from Bob's IP. A millisecond later, Bob's packet arrives at Alice's router. Because the IP and port match the state table, the router lets it through. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hole is punched. Alice and Bob now have a direct, peer-to-peer data stream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Alice's Laptop] ──► (Local Port 3000)
       │
 [Alice's NAT] ────► Opens Hole: Public IP A, Port 50000 
       │ 
       ▼ (Direct UDP Stream) ▲
       │                     │
 [Bob's NAT] ◄─────► Opens Hole: Public IP B, Port 60000
       │
[Bob's Laptop] ◄─── (Local Port 4000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Holepunch Architecture: Cryptographic Routing
&lt;/h3&gt;

&lt;p&gt;In traditional WebRTC architecture, the "Signaling Phase" requires centralized infrastructure: &lt;strong&gt;STUN&lt;/strong&gt; servers (to find your public IP) and &lt;strong&gt;TURN&lt;/strong&gt; servers (to relay traffic if hole punching fails).  If you use STUN/TURN, you are back to relying on centralized cloud providers.The brilliance of the &lt;strong&gt;Holepunch&lt;/strong&gt; stack (and its underlying engine, Bare) is that it throws out STUN and TURN entirely. Instead, it uses &lt;strong&gt;HyperDHT&lt;/strong&gt;, a Kademlia-based Distributed Hash Table.  In this architecture, your public cryptographic key is your IP address.&lt;/p&gt;

&lt;p&gt;1.When Alice boots her application, she joins the DHT. The decentralized nodes in the DHT naturally observe her public IP and port, effectively doing the job of a STUN server without corporate ownership.&lt;br&gt;&lt;br&gt;
2.She announces her 32-byte public key (ed25519) to the swarm.&lt;br&gt;
3.When Bob wants to connect, he doesn't query a DNS server. He searches the DHT for Alice's public key.&lt;br&gt;
4.The DHT nodes coordinate the UDP hole punch. Once the connection is established, Alice and Bob perform a Noise IK handshake to generate a direct, end-to-end encrypted tunnel.&lt;br&gt;&lt;br&gt;
&lt;a href="//hypercore-protocol.github.io"&gt;Visit documentation&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The Code: Bare-Metal P2P
&lt;/h3&gt;

&lt;p&gt;Because Holepunch uses the Bare runtime (stripping away Node.js's heavy HTTP server legacy), establishing this encrypted, serverless connection takes mere lines of code.&lt;/p&gt;

&lt;p&gt;Here is what establishing a serverless, hole-punched P2P connection looks like using hyperdht:&lt;br&gt;
&lt;strong&gt;Alice (The Target):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DHT&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hyperdht&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DHT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyPair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DHT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keyPair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Cryptographic identity&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Direct encrypted P2P stream established!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// The socket is a standard duplex stream. &lt;/span&gt;
  &lt;span class="c1"&gt;// No server required.&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyPair&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Listening on Public Key:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyPair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bob (The Dialer):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DHT&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hyperdht&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DHT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alicePubKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;ALICE_PUBLIC_KEY_HEX&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// The DHT locates Alice, orchestrates the UDP hole punch, &lt;/span&gt;
&lt;span class="c1"&gt;// and establishes the Noise IK encrypted tunnel natively.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alicePubKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Punched through NAT. Connected to Alice.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Unstoppable Web
&lt;/h3&gt;

&lt;p&gt;By utilizing UDP hole punching and a cryptographic DHT, you remove the ultimate single point of failure in modern architecture: the data center.&lt;/p&gt;

&lt;p&gt;You cannot DDoS a network where every user is dynamically routing traffic. You cannot de-platform an application that has no static IP to block. When we talk about true decentralization, it isn't just about putting a financial ledger on a blockchain; it is about reclaiming the physical routing layer of the internet.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>p2p</category>
      <category>networking</category>
      <category>holepunch</category>
    </item>
    <item>
      <title>The Tether Paradox: Shitty ERC-20s, OpenZeppelin, and the Unstoppable Web</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:24:31 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/the-tether-paradox-shitty-erc-20s-openzeppelin-and-the-unstoppable-web-2e5o</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/the-tether-paradox-shitty-erc-20s-openzeppelin-and-the-unstoppable-web-2e5o</guid>
      <description>&lt;p&gt;I have a confession to make. When I first saw that Tether—the behemoth behind the $110 billion USDT stablecoin—was the primary financial backer of Holepunch, Keet, and the Bare JavaScript runtime, my brain short-circuited. &lt;/p&gt;

&lt;p&gt;I struggled with the cognitive dissonance. Why? Because if you have ever written a smart contract that interacts with USDT on Ethereum Mainnet, you know it is an absolute nightmare. &lt;/p&gt;

&lt;p&gt;Before we can talk about Tether’s brilliant vision for a decentralized, serverless future, we have to talk about the trauma they inflicted on a generation of Solidity developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Original Sin: USDT is not actually an ERC-20
&lt;/h3&gt;

&lt;p&gt;When you are deep in protocol-level engineering, you rely on standards. EIP-20 explicitly states that a token's &lt;code&gt;transfer&lt;/code&gt; and &lt;code&gt;transferFrom&lt;/code&gt; functions &lt;em&gt;must&lt;/em&gt; return a boolean value to indicate success or failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The standard EIP-20 Interface
interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tether completely ignored this. When they deployed the USDT contract, they omitted the return value entirely. Their functions return void.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The actual USDT Mainnet Implementation (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uint&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... logic ...&lt;/span&gt;
    &lt;span class="c1"&gt;// Notice: No return statement.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you blindly write a vault or a swap contract using the standard IERC20 interface to move USDT, your transaction will seamlessly execute the logic, move the funds, and then violently revert at the very last microsecond.&lt;/p&gt;

&lt;p&gt;Why? Because modern Solidity uses a strict ABI decoder. When your contract calls USDT.transfer(), the EVM executes a low-level CALL. When the call finishes, Solidity checks the RETURNDATASIZE. Since it expects a bool (32 bytes), but USDT returns absolutely nothing (0 bytes), the decoder panics and reverts the entire transaction.&lt;/p&gt;

&lt;p&gt;For years, the only way to build DeFi safely with USDT has been to wrap it in OpenZeppelin's SafeERC20 library, which uses low-level assembly to explicitly check the return data size and bypass the strict ABI decoding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract Vault {
    using SafeERC20 for IERC20;
    IERC20 public usdt;

    function deposit(uint256 amount) external {
        // We have to use safeTransferFrom because USDT is non-compliant
        usdt.safeTransferFrom(msg.sender, address(this), amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, imagine my skepticism. The company that deployed a non-compliant proxy contract that breaks standard EVM interfaces is now funding the most radical, bare-metal, peer-to-peer web infrastructure of the decade?&lt;/p&gt;

&lt;p&gt;It felt like a bad joke. Until I looked at the system architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Geopolitics of Decentralized Routing
&lt;/h3&gt;

&lt;p&gt;Tether isn't funding Holepunch out of the goodness of their hearts. They are doing it out of existential necessity.&lt;/p&gt;

&lt;p&gt;Look at the architectural dependency graph of the modern financial system. Tether has achieved incredible product-market fit; USDT is the shadow dollar of the global south. But no matter how secure the Ethereum, Tron, or Solana blockchains are, the access layer to those blockchains is controlled by a tiny cartel of Web2 tech monopolies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Traditional Web3 Access Flow ]

User Wallet (Browser) 
      │
      ▼
Cloud-Hosted React Frontend (AWS / Vercel) ──► DNS Providers (Cloudflare)
      │
      ▼
Centralized RPC Node (Infura / Alchemy)
      │
      ▼
The Blockchain (Decentralized)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a sovereign state decides to sanction Tether, they don't have to attack the blockchain. They just force Amazon Web Services to drop the frontend hosting, force Cloudflare to revoke the DNS, and force Infura to block the RPC requests. The blockchain keeps ticking, but the users go dark.&lt;/p&gt;

&lt;p&gt;This is the exact vulnerability Tether is actively engineering out of existence.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Holepunch: Bypassing the Cloud
&lt;/h3&gt;

&lt;p&gt;By funding the Bare runtime and the Holepunch protocol, Tether is building an application delivery layer that completely bypasses centralized cloud infrastructure.&lt;br&gt;&lt;br&gt;
The Bitfinex Blog&lt;/p&gt;

&lt;p&gt;Instead of hosting a trading terminal on AWS, Tether can build a financial application where the binary, the UI, and the data are seeded via Hyperdrive (a BitTorrent-like distributed file system).&lt;/p&gt;

&lt;p&gt;When a user opens the application, they connect directly to other users via a Kademlia Distributed Hash Table (DHT). There is no DNS. There is no cloud server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Holepunch P2P Access Flow ]

Peer A (User) ◄──────── (Noise IK Handshake via UDP) ────────► Peer B (User)
      │                                                           │
      └──► Local Application Logic &amp;amp; State (Seeded via DHT) ◄─────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a blockchain. There is no global consensus, no blocks, and no gas fees. It is simply raw, encrypted, peer-to-peer data replication. Tether is building this to ensure that even if every cloud provider on earth blacklists them, people can still download, run, and communicate through their financial terminals.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architect's Realization
&lt;/h3&gt;

&lt;p&gt;I spent hours frustrated by Tether's sloppy smart contract engineering. But looking at their macro-architecture, I have to respect the pivot.&lt;/p&gt;

&lt;p&gt;They realized that putting a stablecoin on a decentralized ledger is completely useless if the physical wires connecting the users are owned by three centralized corporations. They are funding a non-blockchain JavaScript runtime because they understand that true decentralization requires severing the dependency on the server entirely.&lt;/p&gt;

&lt;p&gt;We spend our time optimizing Solidity gas down to the byte and debating EVM storage slots. But if we don't start thinking about how our applications are actually distributed, we are just building decentralized sandcastles inside a centralized walled garden.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>solidity</category>
      <category>architecture</category>
      <category>tether</category>
    </item>
    <item>
      <title>The Great Web3 Lie: AWS, Infura, and the Return of True Peer-to-Peer</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Sat, 04 Jul 2026 18:26:20 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/the-great-web3-lie-aws-infura-and-the-return-of-true-peer-to-peer-3d8f</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/the-great-web3-lie-aws-infura-and-the-return-of-true-peer-to-peer-3d8f</guid>
      <description>&lt;p&gt;There is a glaring, uncomfortable hypocrisy at the heart of modern Web3 engineering, and we all collectively agree to ignore it. &lt;/p&gt;

&lt;p&gt;We preach the gospel of the "unstoppable web." We write endless threads about censorship resistance, decentralized consensus, and sovereign infrastructure. We build brilliant, mathematically flawless zero-knowledge circuits and deploy them to immutable ledgers. &lt;/p&gt;

&lt;p&gt;And then, to actually let users interact with our cryptographic masterpieces, we wrap them in a React frontend, deploy it to Vercel, and route the transactions through an Infura RPC endpoint running on Amazon Web Services. &lt;/p&gt;

&lt;p&gt;If a centralized cloud provider decides to pull the plug on a data center in us-east-1, half of the "unstoppable" decentralized web goes completely dark. We haven't actually decentralized the internet; we have just decentralized the backend database while leaving the entire application delivery mechanism firmly in the hands of three massive tech monopolies. &lt;/p&gt;

&lt;p&gt;This architectural dissonance has been bothering me for a while. It is what happens when a financial movement (crypto) hijacks an architectural movement (distributed systems). But while digging through alternative runtimes outside of the EVM and Solana ecosystems, I stumbled onto something profoundly disruptive. It isn't a blockchain. It has no token. But it solves the exact infrastructural vulnerability that Web3 ignores. &lt;/p&gt;

&lt;p&gt;It is a stack built by Holepunch—specifically, the &lt;strong&gt;Pear Runtime&lt;/strong&gt; and its underlying engine, &lt;strong&gt;Bare&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When you strip away the marketing, Pear is essentially a modern, hyper-optimized resurrection of the 1990s BitTorrent ethos, applied directly to application development. It forces us to ask a terrifying question: what if true decentralization doesn't require global consensus at all?&lt;/p&gt;

&lt;p&gt;To understand why this is so subversive, you have to look at the traditional deployment model. When you build a modern application, you assume the existence of a server. Even Node.js and Deno fundamentally assume they are running on a machine that will sit in a data center, listen on a port, and serve state to thin clients over HTTP. &lt;/p&gt;

&lt;p&gt;The Holepunch architecture violently rejects this premise. &lt;/p&gt;

&lt;p&gt;When you build an app on Pear, there is no web server. There is no cloud hosting. The application’s source code, assets, and state are distributed via &lt;code&gt;Hyperdrive&lt;/code&gt;, a secure, real-time distributed peer-to-peer file system. When a user "downloads" a Pear application, they aren't fetching it from AWS; they are fetching the encrypted pieces from other users who are currently running the app. &lt;/p&gt;

&lt;p&gt;The moment you open the application, you become a seeder. The deployment infrastructure scales infinitely, automatically, and at zero cost, entirely powered by the aggregate compute of the users themselves. It is serverless, not in the sanitized AWS Lambda sense of the word, but in the literal, anarchic Napster sense. &lt;/p&gt;

&lt;p&gt;Under the hood, this requires a complete teardown of standard networking. You cannot rely on DNS to find a server that doesn't exist. Instead, Holepunch relies on &lt;code&gt;Hyperswarm&lt;/code&gt; and a Kademlia-based Distributed Hash Table (DHT). In this architecture, cryptography isn't used to reach a global financial consensus; it is used purely for routing. Your public cryptographic key becomes your global, static IP address. Devices perform a Noise IK handshake across the DHT, punch through their local NAT firewalls via UDP, and establish a direct, end-to-end encrypted tunnel.&lt;/p&gt;

&lt;p&gt;To make this viable, they had to ditch Node.js. Node is too bloated, carrying decades of legacy APIs designed for centralized web servers. Instead, they built &lt;strong&gt;Bare&lt;/strong&gt;—a stripped-down, modular C-based JavaScript runtime. Bare throws away the HTTP server assumptions. It keeps the V8 engine and the asynchronous event loop, but optimizes entirely for low-level, peer-to-peer data streams across desktop and mobile. &lt;/p&gt;

&lt;p&gt;This architecture fundamentally alters the threat model of the web. You cannot DDoS an application that has no central server. A government cannot compel a cloud provider to take down a user interface if that interface is being seeded dynamically by thousands of autonomous laptops across the globe. &lt;/p&gt;

&lt;p&gt;Web3 promised us an apocalypse-proof internet, but handed us a decentralized ledger heavily tethered to Silicon Valley cloud infrastructure. Seeing a stack like Pear proves that the actual missing layer of the "unstoppable web" was never about consensus mechanisms or tokenomics. It was about raw data availability. &lt;/p&gt;

&lt;p&gt;It turns out, if you want to build truly sovereign software, you have to stop trying to put the server on the blockchain, and simply kill the server altogether. &lt;/p&gt;

&lt;p&gt;Next time, I am going to dive deeper into why the company behind the $100 billion stablecoin USDT (Tether) is the one secretly bankrolling this entire non-blockchain JavaScript ecosystem, because the geopolitical implications of that are wild.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>p2p</category>
      <category>architecture</category>
      <category>systems</category>
    </item>
    <item>
      <title>The Architecture Spiral: RPC, SQL, and the Myth of Linear Evolution</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:42:20 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/the-architecture-spiral-rpc-sql-and-the-myth-of-linear-evolution-3dp7</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/the-architecture-spiral-rpc-sql-and-the-myth-of-linear-evolution-3dp7</guid>
      <description>&lt;p&gt;If you sit in on enough system design meetings, you inevitably witness the exact same debate play out on a repeating loop. It usually starts when a team is breaking down a monolithic backend and trying to figure out how the new microservices should talk to each other. &lt;/p&gt;

&lt;p&gt;Someone suggests standard REST. It’s stateless, ubiquitous, and deeply understood. &lt;br&gt;
Then, a more performance-minded engineer interjects: &lt;em&gt;"JSON over HTTP/1.1 is too heavy for internal service-to-service chatter. We need strict contracts and lower latency. Let's use gRPC."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It feels like a hyper-modern debate—the battle-tested standard of the 2010s versus the cutting-edge, high-performance tooling of the 2020s. &lt;/p&gt;

&lt;p&gt;But if you zoom out, the illusion of modern innovation shatters. Remote Procedure Call (RPC) isn't new. It was conceptualized in the 1970s and standardized in the 1980s. We aren't inventing a new way for computers to communicate; we are just resurrecting a 50-year-old paradigm, dressing it in Protobufs, and multiplexing it over HTTP/2.&lt;/p&gt;

&lt;p&gt;This is the quiet truth of software engineering: &lt;strong&gt;Technological progress is not a straight line. It is a spiral.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We rarely invent entirely new architectures. Instead, we revisit the exact same fundamental concepts, just one layer higher on the Z-axis of abstraction. &lt;/p&gt;




&lt;h2&gt;
  
  
  1. The REST vs. RPC Pendulum
&lt;/h2&gt;

&lt;p&gt;To understand the spiral, look at how we got to REST in the first place. &lt;/p&gt;

&lt;p&gt;In the late 90s and early 2000s, RPC implementations (like CORBA, DCOM, and SOAP) were miserable. They were tightly coupled, brittle, and notoriously difficult to debug across different languages. If a developer updated a method signature on a server, clients across the network would immediately fracture.&lt;/p&gt;

&lt;p&gt;Roy Fielding’s REST (Representational State Transfer) won the web because it provided the ultimate decoupled escape hatch. Instead of executing remote &lt;em&gt;actions&lt;/em&gt;, clients interacted with remote &lt;em&gt;resources&lt;/em&gt; using standardized, predictable HTTP verbs. It was beautiful, cacheable, and heavily adopted.&lt;/p&gt;

&lt;p&gt;But as we transitioned into the era of distributed microservices, REST hit a wall. When you have 40 internal services pinging each other to fulfill a single user request, the overhead of parsing massive JSON payloads and managing stateless HTTP connections becomes a devastating bottleneck. &lt;/p&gt;

&lt;p&gt;So, what did we do? We went right back to RPC. &lt;/p&gt;

&lt;p&gt;We realized that for internal, tightly-bound systems, we actually &lt;em&gt;wanted&lt;/em&gt; strict coupling. We wanted type safety. We wanted binary serialization. Tools like gRPC and tRPC dominate modern backend discussions not because they are conceptually novel, but because they are the 1980s RPC architecture built with 2020s infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Return of the Relational Database
&lt;/h2&gt;

&lt;p&gt;You can see this same spiral evolution in data storage. &lt;/p&gt;

&lt;p&gt;If you were building a startup around 2012, you were explicitly told that relational databases were legacy tech. "SQL is dead," the thought leaders said. "Schema-less NoSQL is web-scale." We poured billions of dollars into document stores like MongoDB and wide-column stores like Cassandra, convinced that abandoning the rigid constraints of tables and foreign keys was the only way to achieve horizontal scalability.&lt;/p&gt;

&lt;p&gt;Fast forward to today. What is the default, undisputed king of databases? &lt;strong&gt;PostgreSQL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not only has SQL made a dominant comeback, but the very NoSQL databases that tried to kill it have spent the last five years desperately bolting SQL-like query languages onto their engines. &lt;/p&gt;

&lt;p&gt;Why did this happen? Because Edgar F. Codd’s "Relational Model of Data," published in 1970, wasn't just a technological trend. It was based on fundamental relational algebra. The math was always correct. The only problem in the 2010s was that the underlying &lt;em&gt;storage engines&lt;/em&gt; struggled to distribute that math across multiple physical servers. &lt;/p&gt;

&lt;p&gt;Once engineers figured out how to build distributed, globally consistent relational engines (like Google Spanner or CockroachDB), the need for NoSQL evaporated for 95% of use cases. We went right back to SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Mainframes to Edge: The Physics of Compute
&lt;/h2&gt;

&lt;p&gt;Perhaps the most massive spiral is where compute actually happens. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 1970s (Centralized):&lt;/strong&gt; The era of the Mainframe. Massive central computers handled all the logic. Users interacted via "dumb terminals" on their desks that did nothing but render the output. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 1990s (Decentralized):&lt;/strong&gt; The era of the Personal Computer. Moore’s Law made chips cheap. We moved compute away from the center and put it directly on the user's desk. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 2010s (Centralized):&lt;/strong&gt; The era of the Cloud. We realized managing thousands of local machines was a nightmare. So, we moved the compute back to massive centralized data centers (AWS, GCP). Our high-powered laptops essentially became very expensive, glowing dumb terminals for web browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 2020s (Decentralized):&lt;/strong&gt; The era of the Edge. The cloud is now too slow for AI inference and high-performance UX. So, via WebAssembly (Wasm), local-first databases (SQLite in the browser), and edge networking, we are pushing the compute right back down to the user's local machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mainframe to PC. Cloud to Edge. It is the exact same architectural breath, inhaling and exhaling across decades.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Spiral?
&lt;/h2&gt;

&lt;p&gt;Why do we constantly go backwards to move forwards? Is it just a lack of imagination? Are we victims of sunk cost, clinging to the paradigms we learned in university?&lt;/p&gt;

&lt;p&gt;No. The spiral exists because software architecture is ultimately bound by physical reality. &lt;/p&gt;

&lt;p&gt;We are constantly negotiating between conflicting, immutable constraints: the speed of light (network latency), the cost of silicon (compute power), and the CAP theorem (consistency vs. availability). &lt;/p&gt;

&lt;p&gt;When we invent a "new" paradigm, we are usually just optimizing for one of these constraints by willingly sacrificing another. When we moved to NoSQL, we sacrificed consistency for availability. When we moved to the Cloud, we sacrificed latency for centralized management. &lt;/p&gt;

&lt;p&gt;Eventually, we push a paradigm to its absolute breaking point. We hit the physical limit of what it can do. And the only way out is to flip the trade-off, looking back into the past for the architecture that optimized for the exact thing we are now starving for.&lt;/p&gt;

&lt;p&gt;We aren't failing to innovate. We are just maturing. We are slowly realizing that the engineers of the 1970s weren't primitive; they were dealing with the exact same fundamental laws of computer science that we are. We just happen to have faster Wi-Fi.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>systems</category>
      <category>web3</category>
    </item>
    <item>
      <title>The Rhythm of the Primitives: Cryptography, Poisson, and the False Memory of the 90s</title>
      <dc:creator>Aniket Misra</dc:creator>
      <pubDate>Tue, 30 Jun 2026 18:28:59 +0000</pubDate>
      <link>https://dev.to/aniket_misra_e47d1564ab7b/the-rhythm-of-the-primitives-cryptography-poisson-and-the-false-memory-of-the-90s-1h1c</link>
      <guid>https://dev.to/aniket_misra_e47d1564ab7b/the-rhythm-of-the-primitives-cryptography-poisson-and-the-false-memory-of-the-90s-1h1c</guid>
      <description>&lt;p&gt;When we look back at the 1990s, our collective cultural memory betrays us. We retroactively paint the decade as an era of primitive digital toys—a neon-tinted landscape of screeching dial-up modems, bulky pagers, and kids feeding pixelated Tamagotchis. We treat it like the infancy of the digital age, a time before real engineering took hold.&lt;/p&gt;

&lt;p&gt;But a funny thing happens when you step away from modern abstractions, sit down with a Rust compiler, and read Satoshi Nakamoto’s 2008 whitepaper in its original context. The illusion shatters. You realize that the foundational architecture of decentralized consensus wasn't invented in the wake of the 2008 financial crisis. Every single piece of the cryptographic puzzle was already breathing, compiling, and thriving in that exact "primitive" decade. &lt;/p&gt;

&lt;p&gt;Satoshi didn't invent Web3. He was just the ultimate systems integrator. &lt;/p&gt;

&lt;p&gt;Digging into the cypherpunk archives of early Usenet groups is like walking into an ancient library and finding the blueprints for a spaceship. The intellectual breeding ground for decentralized technology was fully formed while the rest of the world was struggling to load JPEGs. The concept of sovereign-less "electronic money" was relentlessly debated. Wei Dai’s B-money and Nick Szabo’s Bit Gold laid the philosophical groundwork. The data structures required to keep a ledger lightweight—Merkle Trees—had already been patented by Ralph Merkle back in 1979. &lt;/p&gt;

&lt;p&gt;The missing link was always sybil resistance. How do you stop a malicious actor from spinning up a million fake identities to rewrite the ledger? The answer was sitting in 1997 with Adam Back’s Hashcash. Hashcash wasn't designed for global finance; it was a clever, brute-force hack to make email spam economically unviable by forcing a computer to expend CPU cycles to calculate a cryptographic hash. &lt;/p&gt;

&lt;p&gt;Satoshi took this anti-spam mechanism, lifted it from the forgotten corners of the 90s internet, and weaponized it into the heartbeat of global consensus. &lt;/p&gt;

&lt;p&gt;But where the whitepaper transitions from a clever engineering integration into an absolute masterpiece is in its mathematical rigor. Satoshi didn't just theorize that a decentralized network was secure; he proved it statistically. In the quietest, most devastatingly elegant section of the paper, he addresses the system's core vulnerability—a 51% attack—by framing it as a Binomial Random Walk. &lt;/p&gt;

&lt;p&gt;It is the classic Gambler’s Ruin problem applied to global finance. If an honest node has a probability (p) of finding the next block, and a malicious attacker has a probability (q, which is 1-p, that is, unless we are in a universe with probabilities&amp;gt;100%), the math becomes a beautiful, brutal reality. Because block discovery is probabilistic, Satoshi maps the attacker's potential progress to a Poisson distribution. &lt;/p&gt;

&lt;p&gt;He proves, with cold mathematical certainty, that as long as honest nodes control more CPU power (p &amp;gt; q), the probability of an attacker successfully rewriting history drops exponentially with every passing block. It is a masterpiece of statistical poetry. He built a trustless system by mathematically proving that betrayal is too expensive.&lt;/p&gt;

&lt;p&gt;Uncovering these anachronisms in technology—realizing that the heavy cryptographic lifting was happening parallel to the Tamagotchi—triggered a weird cognitive dissonance. It made me question what else I was misremembering about that era. Being a musician, I naturally turned to the rhythm of the decade.&lt;/p&gt;

&lt;p&gt;For my entire life, I have retrospectively framed the 90s as the decade of relentless, underground Eurotechno. In my head, the soundtrack of that era was an endless loop of Haddaway’s "What is Love" thumping out of a dark, synthesized European club basement. I equated the 90s with the four-on-the-floor kick drum of early electronic dance music.&lt;/p&gt;

&lt;p&gt;I was completely wrong. Look at the actual cultural footprint of the decade. It wasn't defined by cold European synthesizers; it was a massively hispanophile era. The &lt;em&gt;Macarena&lt;/em&gt; didn't just exist; it conquered the globe. Latin pop exploded into the global mainstream. The rhythm of the decade was fundamentally organic, percussive, and intensely Latin. The aesthetic I had projected onto the past was a complete fabrication.&lt;/p&gt;

&lt;p&gt;This historical unearthing has bled into my own physical reality. I’ve realized that my own rhythms need a structural shift. The tabla is a beautiful, complex instrument, and it will always be a part of my percussive foundation. But you cannot lug a heavy set of brass and wood up a mountain on a hike. It is geographically anchoring. &lt;/p&gt;

&lt;p&gt;So, I’m reviving my old school assembly chops. I am packing up the bongos. They fit in a backpack. They carry that organic, Afro-Cuban groove that actually defined the era I’ve been misremembering. And to make sure I’m not just an isolated rhythm section on the trail, I’ve picked up a harmonica to build out some melodic grit. &lt;/p&gt;

&lt;p&gt;Afro-Cuban percussion and delta Blues are officially on. It turns out, whether you are integrating 1990s cypherpunk primitives to build decentralized state machines, or blending bongos with a blues harp on a hiking trail, the magic is never in inventing something entirely new. The magic is in the assembly. &lt;/p&gt;

&lt;p&gt;Time to get back to the Rust compiler.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>history</category>
      <category>music</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
