<?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: GDS K S</title>
    <description>The latest articles on DEV Community by GDS K S (@thegdsks).</description>
    <link>https://dev.to/thegdsks</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3592860%2F7dec468f-4f91-4b1d-9d24-99091e204707.jpg</url>
      <title>DEV Community: GDS K S</title>
      <link>https://dev.to/thegdsks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thegdsks"/>
    <language>en</language>
    <item>
      <title>TanStack shipped a postmortem for the 42-package npm compromise. Here is what every project should change this week.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Fri, 29 May 2026 02:33:59 +0000</pubDate>
      <link>https://dev.to/thegdsks/tanstack-shipped-a-postmortem-for-the-42-package-npm-compromise-here-is-what-every-project-should-60c</link>
      <guid>https://dev.to/thegdsks/tanstack-shipped-a-postmortem-for-the-42-package-npm-compromise-here-is-what-every-project-should-60c</guid>
      <description>&lt;h1&gt;
  
  
  TanStack shipped a postmortem for the 42-package npm compromise. Here is what every project should change this week.
&lt;/h1&gt;

&lt;p&gt;On May 11, 2026, between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 packages in the &lt;code&gt;@tanstack&lt;/code&gt; scope. The attacker did not steal a maintainer's npm credentials. They hijacked the build pipeline itself, and the packages they shipped carried valid SLSA provenance attestations. That last part changes something important about how the ecosystem thinks about supply chain trust.&lt;/p&gt;

&lt;p&gt;TanStack published a full postmortem. This piece walks through the attack chain, explains what made this incident novel, and gives you a concrete checklist for your own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;May 11, 2026, 19:20 to 19:26 UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;42 @tanstack packages, 84 malicious versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worm reach&lt;/td&gt;
&lt;td&gt;170+ packages total after self-propagation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;External researcher flagged it within 6 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full deprecation&lt;/td&gt;
&lt;td&gt;~1 hour 43 minutes after first publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advisory&lt;/td&gt;
&lt;td&gt;GHSA-g7cv-rxg3-hmpx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Novel claim&lt;/td&gt;
&lt;td&gt;First documented malicious npm package carrying valid SLSA provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. What happened and when
&lt;/h2&gt;

&lt;p&gt;The attacker, operating under accounts &lt;code&gt;zblgg&lt;/code&gt; and &lt;code&gt;voicproducoes&lt;/code&gt;, targeted the TanStack Router/Start monorepo. The Query, Table, Form, Virtual, Store, and AI packages were not affected. Only the Router/Start monorepo contained the vulnerable workflow configuration.&lt;/p&gt;

&lt;p&gt;At 19:20 UTC the first malicious versions landed. By 19:26 the full 84-version batch hit the registry. An external researcher named ashishkurmi from StepSecurity spotted the anomaly, an unusual &lt;code&gt;optionalDependencies&lt;/code&gt; entry pointing to a GitHub fork, within minutes. No internal alerting triggered on TanStack's side.&lt;/p&gt;

&lt;p&gt;TanStack deprecated the malicious versions 1 hour 43 minutes after the first publish. npm pulled the tarballs from 22:13 to 23:55 UTC, a 4.5-hour window after the initial compromise.&lt;/p&gt;

&lt;p&gt;The payload was a 2.3 MB obfuscated file named &lt;code&gt;router_init.js&lt;/code&gt;. It harvested credentials (GitHub tokens, AWS keys, Vault tokens, Kubernetes service accounts, SSH keys, GCP credentials), exfiltrated them over the Session/Oxen P2P messenger network, and then used any stolen publish-capable tokens to republish itself to every other package the victim could write to. It also installed persistence mechanisms in &lt;code&gt;.claude/settings.json&lt;/code&gt; hooks, VS Code task injection, and a systemd monitoring service. If the stolen GitHub token was later revoked, the payload wiped the home directory.&lt;/p&gt;

&lt;p&gt;Secondary victims included &lt;code&gt;@mistralai/mistralai&lt;/code&gt;, 40-plus &lt;code&gt;@uipath&lt;/code&gt; packages, and 19 packages in aviation-related namespaces. Wiz attributes the campaign, named "Mini Shai-Hulud" internally, to a threat group called TeamPCP, linked to prior SAP, Checkmarx, and Trivy compromises.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The three-primitive attack chain
&lt;/h2&gt;

&lt;p&gt;Most supply chain coverage stops at "compromised package." The TanStack incident is worth studying in detail because the attacker chained three distinct primitives to get from zero access to a signed publish on a major open-source project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Primitive 1: The Pwn Request
&lt;/h3&gt;

&lt;p&gt;A "Pwn Request" is a specific GitHub Actions anti-pattern. When a workflow uses &lt;code&gt;pull_request_target&lt;/code&gt; as its trigger, it runs in the context of the base repository rather than the fork. That means it has access to base repository secrets. The intent of &lt;code&gt;pull_request_target&lt;/code&gt; is to let maintainers do things like post comments on pull requests from forks without exposing write tokens to fork code.&lt;/p&gt;

&lt;p&gt;The problem: if the workflow also checks out the pull request's code and executes it, you get fork code running with base repository privileges. TanStack's &lt;code&gt;bundle-size.yml&lt;/code&gt; workflow had this pattern.&lt;/p&gt;

&lt;p&gt;The attacker opened a PR from a fork. The workflow executed the fork's code with base repo context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Primitive 2: Cache poisoning across trust boundaries
&lt;/h3&gt;

&lt;p&gt;The malicious fork code poisoned the pnpm package store cache. It wrote a 1.1 GB cache entry under the exact key that the legitimate &lt;code&gt;release.yml&lt;/code&gt; workflow would later restore.&lt;/p&gt;

&lt;p&gt;This is the trust-boundary crossing. The bundle-size workflow (lower trust, triggered by PRs) and the release workflow (higher trust, triggered by maintainer merges) shared a cache key namespace. The attacker wrote to cache from the low-trust context. The high-trust context read from it without re-validating.&lt;/p&gt;

&lt;p&gt;The poisoned cache entry sat undetected for eight hours before the release workflow pulled it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Primitive 3: OIDC token extraction from runner memory
&lt;/h3&gt;

&lt;p&gt;Here is the part that bypasses npm credential protections entirely.&lt;/p&gt;

&lt;p&gt;GitHub Actions supports OIDC-based publishing. Instead of storing a long-lived npm token in your repository secrets, your workflow requests a short-lived OIDC token from GitHub at publish time. npm's trusted publisher feature accepts this token. The design assumes that only the intended workflow step can request and use that token.&lt;/p&gt;

&lt;p&gt;The attacker's payload included binaries that read &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mem&lt;/code&gt; on the GitHub Actions runner. Processes in the runner environment, including the GitHub Actions agent, hold the OIDC token in memory while the job runs. The attacker extracted that token directly from memory and used it to authenticate npm publishes, bypassing the actual publish step in the release workflow.&lt;/p&gt;

&lt;p&gt;This is why the packages carried valid SLSA provenance attestations. The attestation records that the package shipped from the expected repository and workflow. From Sigstore's perspective, that was true. The attacker did not forge the attestation. They hijacked the pipeline mid-run and minted legitimate credentials within it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why valid SLSA provenance on a malicious package matters
&lt;/h2&gt;

&lt;p&gt;SLSA (Supply chain Levels for Software Artifacts) provenance is one of the main signals the npm ecosystem has been building toward for trusted package distribution. The idea: a package with SLSA provenance attestation proves it came from a specific source commit in a specific workflow. Consumers can verify this cryptographically.&lt;/p&gt;

&lt;p&gt;The TanStack incident stands as the first documented case of a malicious npm package carrying SLSA provenance that the attacker did not forge. Sigstore verified the build correctly. The provenance was real. The code running through the pipeline was not safe.&lt;/p&gt;

&lt;p&gt;SLSA provenance answers the question "did this package build how the maintainer intended?" It does not answer "did the build pipeline run clean before the build started?" Those are different questions, and the ecosystem has largely treated them as the same question.&lt;/p&gt;

&lt;p&gt;This does not make SLSA provenance worthless. A package with no provenance is less trustworthy than one with provenance. But it does mean provenance is a necessary condition, not a complete one. The signal has a new attack surface.&lt;/p&gt;

&lt;p&gt;What a cleaner version of SLSA provenance would need: a way to attest that the cache state restored before the build arrived clean, that no cross-context cache sharing occurred, and that OIDC token issuance covered only a specific workflow step rather than any code running in the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Lockdown checklist for your project this week
&lt;/h2&gt;

&lt;p&gt;Run through this before your next release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit your package-lock for affected versions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for any @tanstack packages from May 11 UTC&lt;/span&gt;
npm audit
npx better-npm-audit audit

&lt;span class="c"&gt;# List all @tanstack versions currently installed&lt;/span&gt;
npm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 | &lt;span class="nb"&gt;grep &lt;/span&gt;tanstack

&lt;span class="c"&gt;# Verify against the advisory&lt;/span&gt;
&lt;span class="c"&gt;# Affected: @tanstack/* versions published 2026-05-11 between 19:20-23:55 UTC&lt;/span&gt;
&lt;span class="c"&gt;# Safe: any version before May 11 or after npm confirmed tarball removal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you pulled a new install or ran CI between May 11 19:20 UTC and May 11 23:55 UTC, treat your build environment as potentially compromised. Rotate any credentials that were present in that environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Harden your GitHub Actions workflows
&lt;/h3&gt;

&lt;p&gt;The Pwn Request pattern is the root primitive. Audit every workflow file for &lt;code&gt;pull_request_target&lt;/code&gt; triggers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# DANGEROUS: pull_request_target that checks out and runs fork code&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;  &lt;span class="c1"&gt;# THIS IS THE PROBLEM&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/span&gt;  &lt;span class="c1"&gt;# fork code running with base repo context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SAFER: split into two workflows&lt;/span&gt;
&lt;span class="c1"&gt;# Workflow 1: runs on pull_request (fork context, no secrets)&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;  &lt;span class="c1"&gt;# checks out fork code, no secret access&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npm run build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pr-artifacts&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./dist&lt;/span&gt;

&lt;span class="c1"&gt;# Workflow 2: runs on workflow_run (base context, has secrets, reads artifacts not code)&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PR"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;  &lt;span class="c1"&gt;# reads build output, not fork code&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pr-artifacts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need &lt;code&gt;pull_request_target&lt;/code&gt; for a legitimate reason (bot comments, label management), never check out PR code in that context. Keep it to read-only GitHub API calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope your OIDC token permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Restrict permissions at the job level, not just the workflow level&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;    &lt;span class="c1"&gt;# only the publish job gets OIDC&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish --provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not grant &lt;code&gt;id-token: write&lt;/code&gt; at the workflow level if only one job needs it. The narrower the scope, the shorter the window an extracted token stays useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolate your cache keys by trust level
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Separate cache keys for PR workflows vs release workflows&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.pnpm-store&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release-pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}&lt;/span&gt;
    &lt;span class="c1"&gt;# Never share this key with pull_request_target workflows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use different key prefixes for PR-triggered and release-triggered workflows. A compromised PR workflow cannot poison a release workflow's cache if the keys do not overlap. This is not a full defense (an attacker with arbitrary code execution can still do damage), but it eliminates the specific cache-poisoning vector used here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check for persistence artifacts if you ran a CI job during the window
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for the gh-token-monitor service (one of the payload's persistence mechanisms)&lt;/span&gt;
systemctl status gh-token-monitor 2&amp;gt;/dev/null
&lt;span class="nb"&gt;ls&lt;/span&gt; ~/.local/share/systemd/user/ | &lt;span class="nb"&gt;grep &lt;/span&gt;monitor

&lt;span class="c"&gt;# Check VS Code tasks for injected entries&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; .vscode/tasks.json 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; monitor

&lt;span class="c"&gt;# Check Claude settings for hook injection&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.claude/settings.json 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'"permissions"'&lt;/span&gt;

&lt;span class="c"&gt;# If you find any of these: stop, rotate credentials first, then remove&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload's wiper triggers when someone revokes a stolen token while the daemon runs. Confirm the daemon is not present before rotating credentials, or coordinate both actions at the same instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What changes downstream if provenance is not a clean signal
&lt;/h2&gt;

&lt;p&gt;Practically, for most teams consuming public packages, the immediate answer is: not much changes in workflow, but the mental model needs updating.&lt;/p&gt;

&lt;p&gt;Provenance attestation was the "this package came from a known clean pipeline" signal. That signal is now more accurately described as "this package came from the expected repository and workflow, assuming the pipeline itself was not injected into." For widely-used OSS packages where you have no visibility into the upstream CI environment, that assumption deserves scrutiny.&lt;/p&gt;

&lt;p&gt;Three things worth watching in the next quarter:&lt;/p&gt;

&lt;p&gt;First, whether npm or the SLSA spec adds guidance on cache attestation. The build pipeline audit trail currently does not record what cache state was restored before the build ran. Adding that would let downstream consumers see whether a restore happened and from what source.&lt;/p&gt;

&lt;p&gt;Second, whether GitHub adds controls to block OIDC token issuance from jobs that restored cache from a lower-trust workflow. Right now the runner process holds the token regardless of how the cache arrived. A job-level flag to drop OIDC access after a cross-context cache restore would close this specific vector.&lt;/p&gt;

&lt;p&gt;Third, whether teams start treating &lt;code&gt;@ts-nocheck&lt;/code&gt; and &lt;code&gt;skip audit&lt;/code&gt; patterns in CI the same way they treat the Pwn Request pattern: as defaults that need an explicit justification written next to them. The TanStack postmortem credits an external researcher with the detection. The internal system had no alert. That is the gap to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;TanStack's maintainers handled this well. They published a detailed timeline, named the advisory, credited the researcher, and documented what their internal detection missed. That level of transparency under pressure is worth acknowledging.&lt;/p&gt;

&lt;p&gt;The incident is notable for two reasons. One is scale: 12.7 million weekly downloads on &lt;code&gt;@tanstack/react-router&lt;/code&gt; alone means a narrow six-minute window had real blast radius potential. The other is the SLSA provenance angle. The attacker did not break the signature. They got inside the signing process.&lt;/p&gt;

&lt;p&gt;If your project uses GitHub Actions for publishing, run the workflow audit above before your next release. The Pwn Request pattern is common, the cache isolation gap is invisible until something like this happens, and the OIDC scoping is easy to miss in a busy workflow file. None of these fixes take more than an afternoon.&lt;/p&gt;

&lt;p&gt;How does your team currently handle CI trust boundaries between PR workflows and release workflows? Drop your setup in the comments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Valid provenance on a malicious package is not a cryptography failure. Pipeline isolation failed.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Google's Gemini 3.5 Flash is 4x faster than other frontier models. Here is how to call it from TypeScript.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Wed, 27 May 2026 17:20:41 +0000</pubDate>
      <link>https://dev.to/thegdsks/googles-gemini-35-flash-is-4x-faster-than-other-frontier-models-here-is-how-to-call-it-from-2ih5</link>
      <guid>https://dev.to/thegdsks/googles-gemini-35-flash-is-4x-faster-than-other-frontier-models-here-is-how-to-call-it-from-2ih5</guid>
      <description>&lt;h1&gt;
  
  
  Google's Gemini 3.5 Flash is 4x faster than other frontier models. Here is how to call it from TypeScript.
&lt;/h1&gt;

&lt;p&gt;Google shipped Gemini 3.5 Flash on May 19 at Google I/O 2026. The headline claim is four times faster output tokens per second compared to other frontier models. That is not a marketing tier label. The claim is a throughput number, and for latency-sensitive work like streaming chat, code generation, or agentic loops, it changes what is worth reaching for.&lt;/p&gt;

&lt;p&gt;Here is what the model actually is, how to wire it up in TypeScript, and what the cost and rate limit picture looks like before you depend on it in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Gemini 3.5 Flash&lt;/th&gt;
&lt;th&gt;Gemini 2.5 Flash&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output speed&lt;/td&gt;
&lt;td&gt;4x faster than other frontier models&lt;/td&gt;
&lt;td&gt;Best price-performance for high-volume tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary use&lt;/td&gt;
&lt;td&gt;Agentic workflows, coding, long-horizon tasks&lt;/td&gt;
&lt;td&gt;Cost-sensitive, high-volume, reasoning tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input price&lt;/td&gt;
&lt;td&gt;$1.50 per 1M tokens&lt;/td&gt;
&lt;td&gt;$0.30 per 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output price&lt;/td&gt;
&lt;td&gt;$9.00 per 1M tokens&lt;/td&gt;
&lt;td&gt;$2.50 per 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Yes (limited)&lt;/td&gt;
&lt;td&gt;Yes (standard rate limits)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDK package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@google/genai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@google/genai&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Released&lt;/td&gt;
&lt;td&gt;May 19, 2026&lt;/td&gt;
&lt;td&gt;Earlier in 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. What Gemini 3.5 Flash is and where it fits
&lt;/h2&gt;

&lt;p&gt;Google positions Gemini 3.5 Flash as the fast tier in the 3.5 family. The framing from the announcement is "frontier intelligence with action," which is a wordy way of saying: this model runs complex agentic tasks at a speed where the latency is not the bottleneck anymore.&lt;/p&gt;

&lt;p&gt;The benchmarks Google published back this up. On Terminal-Bench 2.1, 3.5 Flash scores 76.2%. On MCP Atlas it hits 83.6%. On CharXiv Reasoning, a multimodal benchmark, it reaches 84.2%. Google published those scores for agentic and coding workloads, not general chat.&lt;/p&gt;

&lt;p&gt;Where does it fit against the rest of the lineup? The 2.5 Flash is cheaper per token and designed for high-volume reasoning tasks where cost per call matters more than raw throughput. The 3.5 Flash costs more but delivers output fast enough that the wall-clock time for an agentic loop shrinks, which can lower your per-task cost even at a higher per-token rate. Google's own framing is "often at less than half the cost of other frontier models" for full tasks, not individual calls.&lt;/p&gt;

&lt;p&gt;For most TypeScript projects, the decision point is: does your user wait for the output, or does a pipeline consume it? If a user is staring at a cursor, speed matters and 3.5 Flash is worth the price premium. If a background job is processing documents at scale, 2.5 Flash is likely the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Install the SDK and make your first call
&lt;/h2&gt;

&lt;p&gt;The SDK is &lt;code&gt;@google/genai&lt;/code&gt;. Node.js 18 or later required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @google/genai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set your API key from &lt;a href="https://aistudio.google.com" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-key-here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basic call:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&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="s2"&gt;@google/genai&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;ai&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;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&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;response&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;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarize the key breaking changes in Node.js 22 for a TypeScript developer.&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;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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole surface for a one-shot request. The &lt;code&gt;GoogleGenAI&lt;/code&gt; constructor accepts the key directly or reads &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; from the environment when called with an empty object &lt;code&gt;{}&lt;/code&gt;. Prefer the explicit key reference so your intent is clear at the call site.&lt;/p&gt;

&lt;p&gt;Worth noting: &lt;code&gt;response.text&lt;/code&gt; is a convenience accessor. The full response tree lives at &lt;code&gt;response.candidates[0].content.parts&lt;/code&gt;. You only need to go that deep when handling multi-modal outputs or function call responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Streaming responses
&lt;/h2&gt;

&lt;p&gt;Four times faster output speed matters most when you stream. A blocking &lt;code&gt;generateContent&lt;/code&gt; call holds the connection open until the model finishes. For a 1,000-token response at high throughput, that is still a perceivable wait for a user. Streaming pipes each chunk to the client as the model produces it.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&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="s2"&gt;@google/genai&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;ai&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;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamToStdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;stream&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;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContentStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&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;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&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="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="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;await&lt;/span&gt; &lt;span class="nf"&gt;streamToStdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write a TypeScript function that retries a promise up to N times with exponential backoff.&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;In a Next.js API route or an Express server, you would pipe &lt;code&gt;chunk.text&lt;/code&gt; into a &lt;code&gt;ReadableStream&lt;/code&gt; and set &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;. The pattern is the same: iterate the async generator, forward each chunk.&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;// pages/api/generate.ts (Next.js App Router example)&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;NextRequest&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="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;GoogleGenAI&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="s2"&gt;@google/genai&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;ai&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;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&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;NextRequest&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;prompt&lt;/span&gt; &lt;span class="p"&gt;}&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;stream&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;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContentStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;readable&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;ReadableStream&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;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&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="nx"&gt;controller&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;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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;text/plain; charset=utf-8&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 4x throughput claim shows up in the time between the first chunk and the last. At high output speeds, the stream feels snappy from the user's side even when total token count is large.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tool calling in TypeScript
&lt;/h2&gt;

&lt;p&gt;Gemini 3.5 Flash handles function calling with a three-step cycle: you declare the tool, the model returns a function call request, you execute and send back the result.&lt;/p&gt;

&lt;p&gt;One thing to know before you write any code: Gemini 3 model APIs attach a unique &lt;code&gt;id&lt;/code&gt; to every function call. You must echo that &lt;code&gt;id&lt;/code&gt; back in the function response or the model cannot match results to calls. This changed in the 3.x API line.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Type&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="s2"&gt;@google/genai&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;ai&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;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Step 1: Declare the tool&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getWeatherDeclaration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_weather&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Returns current weather conditions for a city.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&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="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OBJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&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="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;City name, e.g. Tokyo&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;units&lt;/span&gt;&lt;span class="p"&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="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Temperature unit: celsius or fahrenheit&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;city&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="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: Send the initial request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What is the weather in Oslo right now?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;functionDeclarations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;getWeatherDeclaration&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="c1"&gt;// Step 3: Handle the function call&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&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;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&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;// Your real implementation here&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weatherData&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;fetchWeatherFromYourAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;units&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Build conversation history with the function result&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What is the weather in Oslo right now?&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="o"&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;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;parts&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="na"&gt;functionResponse&lt;/span&gt;&lt;span class="p"&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="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Required in Gemini 3.x&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&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="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;weatherData&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 4: Get the final natural-language response&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;final&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;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;functionDeclarations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;getWeatherDeclaration&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="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="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWeatherFromYourAPI&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="nl"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;units&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Placeholder. Replace with your actual weather API call.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cloudy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;city&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="nx"&gt;city&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;Two practical notes. The &lt;code&gt;Type&lt;/code&gt; enum imported from &lt;code&gt;@google/genai&lt;/code&gt; is mandatory for the parameter schema. Do not pass raw strings like &lt;code&gt;"object"&lt;/code&gt; for the type field. The model also accepts an array of tool declarations, and you can include more than one function if your agentic workflow needs to route between them.&lt;/p&gt;

&lt;p&gt;For parallel tool calls in a single turn, the model may return more than one entry in &lt;code&gt;response.functionCalls&lt;/code&gt;. Iterate the array, execute each, and send all results back in one follow-up request.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cost and rate limits
&lt;/h2&gt;

&lt;p&gt;The pricing numbers above in the TL;DR table come from Google AI Studio's pricing page as of May 2026. Two practical caveats before you budget anything.&lt;/p&gt;

&lt;p&gt;Gemini 3.5 Flash costs $1.50 per million input tokens and $9.00 per million output tokens on the paid tier. Output pricing includes thinking tokens if the model uses internal reasoning steps. In a chat or code-generation workflow, output typically runs 2 to 4 times the input token count, so budget accordingly.&lt;/p&gt;

&lt;p&gt;The 2.5 Flash at $0.30 input / $2.50 output is a meaningful difference at scale. A task that generates 10,000 output tokens costs $0.025 on 2.5 Flash and $0.09 on 3.5 Flash. That is 3.6x more per call. The gap can close if the 4x speed advantage means 3.5 Flash completes a multi-turn agentic task in fewer wall-clock seconds and the task itself needs fewer total tokens because the model gets there faster. Test against your actual workload rather than extrapolating from single-call pricing.&lt;/p&gt;

&lt;p&gt;Both models have a free tier through the Gemini API with rate limits Google does not publish precisely on the pricing page. The paid tier removes the per-day caps. If you are prototyping, the free tier is enough. If you are running production traffic, use a paid project and set a monthly spend cap in the Google Cloud console.&lt;/p&gt;

&lt;p&gt;One hard ceiling worth knowing: Google Search grounding requests share a 5,000 prompt monthly quota across all Gemini 3 models on the free tier, then $14 per 1,000 queries on paid. If your tool-calling setup routes through Search grounding, that quota burns faster than you expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The bottom line
&lt;/h2&gt;

&lt;p&gt;Gemini 3.5 Flash is worth adding to your model comparison list. Google's own benchmarks back the 4x output speed claim, and the numbers line up with the agentic workload focus. The TypeScript SDK is straightforward. The function calling API has one new rule compared to older Gemini versions: always echo the &lt;code&gt;id&lt;/code&gt; field back in your function response.&lt;/p&gt;

&lt;p&gt;The price premium over 2.5 Flash is real. Whether it pays back depends on whether your users wait for output and whether your agentic loops shrink enough in wall-clock time to offset the per-token cost difference. Run both models against your actual task shape before committing either to production.&lt;/p&gt;

&lt;p&gt;What kind of workload are you considering Gemini 3.5 Flash for? Drop a comment, especially if you have run latency comparisons against other frontier models.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Speed is only free if you would have paid for the wall-clock time anyway.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build your first MCP server in TypeScript: the 2026 setup that takes 30 minutes.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Tue, 26 May 2026 20:07:21 +0000</pubDate>
      <link>https://dev.to/thegdsks/build-your-first-mcp-server-in-typescript-the-2026-setup-that-takes-30-minutes-3m1n</link>
      <guid>https://dev.to/thegdsks/build-your-first-mcp-server-in-typescript-the-2026-setup-that-takes-30-minutes-3m1n</guid>
      <description>&lt;h1&gt;
  
  
  Build your first MCP server in TypeScript: the 2026 setup that takes 30 minutes.
&lt;/h1&gt;

&lt;p&gt;I had Claude Desktop open. I needed it to query a local SQLite database without copy-pasting schema dumps into the chat. Thirty minutes later I had a working MCP server. Here is the exact path I took, stripped of dead ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you build&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project setup&lt;/td&gt;
&lt;td&gt;npm project, tsconfig, SDK install&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First tool&lt;/td&gt;
&lt;td&gt;Structured input, structured output&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First resource&lt;/td&gt;
&lt;td&gt;Read-only data the model can request&lt;/td&gt;
&lt;td&gt;8 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect Claude Desktop&lt;/td&gt;
&lt;td&gt;Config file, restart, verify&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common pitfalls&lt;/td&gt;
&lt;td&gt;Avoid the three bugs that kill every first attempt&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What MCP actually is
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol is a standard for connecting AI models to external data and tools. The model issues requests, your server handles them, and the results come back in a format the model understands. That is the whole idea.&lt;/p&gt;

&lt;p&gt;Before MCP, every tool integration was custom. OpenAI had function calling. Anthropic had tool use. Cursor had its own plugin format. MCP standardizes the wire protocol so you write one server and any compliant client can call it, whether that is Claude Desktop, Cursor, or a client you build yourself.&lt;/p&gt;

&lt;p&gt;The three primitives you care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resources: read-only data the model can fetch, like files or database rows.&lt;/li&gt;
&lt;li&gt;Tools: functions the model can call with arguments, like running a query or sending a request.&lt;/li&gt;
&lt;li&gt;Prompts: reusable prompt templates the client can surface to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial covers tools and resources. Prompts follow the same pattern and you will not need them for most servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Project setup
&lt;/h2&gt;

&lt;p&gt;Node 18 or higher required. Check with &lt;code&gt;node --version&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-mcp-server &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-mcp-server
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @modelcontextprotocol/sdk zod
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript @types/node
&lt;span class="nb"&gt;mkdir &lt;/span&gt;src
&lt;span class="nb"&gt;touch &lt;/span&gt;src/index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK package is &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;. The version on npm as of May 2026 is 1.11.x. Zod handles schema validation for tool inputs.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;package.json&lt;/code&gt; with these fields:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node build/index.js"&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;Create &lt;code&gt;tsconfig.json&lt;/code&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"include"&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;"src/**/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&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;"node_modules"&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;h2&gt;
  
  
  2. Implementing a tool
&lt;/h2&gt;

&lt;p&gt;A tool is a function the model can call. You define its name, description, input schema, and handler. The model reads the description and schema to decide when and how to call it.&lt;/p&gt;

&lt;p&gt;Here is a complete server with one tool that converts a hex color to RGB:&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;// src/index.ts&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;McpServer&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="s2"&gt;@modelcontextprotocol/sdk/server/mcp.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;StdioServerTransport&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="s2"&gt;@modelcontextprotocol/sdk/server/stdio.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;z&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="s2"&gt;zod&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;color-tools&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&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;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex_to_rgb&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="s2"&gt;Convert a hex color string to RGB components. Input must include the leading #.&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;hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^#&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-fA-F&lt;/span&gt;&lt;span class="se"&gt;]{6}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Must be a 6-digit hex color, e.g. #ff5733&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;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hex&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&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;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&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="na"&gt;content&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="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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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="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;transport&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;StdioServerTransport&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;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;p&gt;The description string is what the model reads to decide whether to call the tool. Write it as plainly as you would write a JSDoc comment for a teammate. Vague descriptions produce missed calls or wrong inputs.&lt;/p&gt;

&lt;p&gt;The second argument to &lt;code&gt;server.tool()&lt;/code&gt; is the description. The third is a Zod schema object. The SDK turns this into a JSON Schema that the client sends to the model. Keep schemas tight: required fields only, no optional fields that do not change the output.&lt;/p&gt;

&lt;p&gt;The return value must have a &lt;code&gt;content&lt;/code&gt; array. Each item has a &lt;code&gt;type&lt;/code&gt; and a &lt;code&gt;text&lt;/code&gt; (or &lt;code&gt;data&lt;/code&gt; for binary). Return JSON as a string inside a text item. The model can parse it from there.&lt;/p&gt;

&lt;p&gt;Build and test locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'&lt;/span&gt; | node build/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a JSON-RPC response listing &lt;code&gt;hex_to_rgb&lt;/code&gt;. That confirms the server starts and responds to the list request.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Implementing a resource
&lt;/h2&gt;

&lt;p&gt;Resources expose read-only data the model can pull on demand. A common use case: expose the schema of your local database so the model knows the table structure before writing a query.&lt;/p&gt;

&lt;p&gt;Add this before the transport setup:&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db-schema&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="s2"&gt;sqlite:///local.db&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&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="c1"&gt;// In a real server, read this from your database&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  created_at INTEGER NOT NULL
);
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  user_id INTEGER REFERENCES users(id),
  total_cents INTEGER NOT NULL,
  placed_at INTEGER NOT NULL
);
    `&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="na"&gt;contents&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="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first argument is the resource name. The second is the URI the client uses to request it. Pick a URI scheme that makes sense for your data: file, sqlite, https, or a custom scheme like &lt;code&gt;myapp://&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Resources are pull-based. The model requests them when it decides it needs them. If you want data pushed into every conversation automatically, that is a different pattern (system prompt injection at the client level, not a resource).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Hooking it up to Claude Desktop
&lt;/h2&gt;

&lt;p&gt;Build the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open your Claude Desktop config file. On macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your server to the &lt;code&gt;mcpServers&lt;/code&gt; block:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"color-tools"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"/absolute/path/to/my-mcp-server/build/index.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="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;Use the absolute path. Relative paths fail silently, which is the single most common first-timer mistake. Restart Claude Desktop fully (quit from the menu bar, not just close the window). Open a new conversation. You should see a hammer icon in the input bar indicating tools are available. Type "convert #3b82f6 to RGB" and watch it call the tool.&lt;/p&gt;

&lt;p&gt;For Cursor, the config lives at &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; and uses the same &lt;code&gt;mcpServers&lt;/code&gt; JSON shape:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"color-tools"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"/absolute/path/to/my-mcp-server/build/index.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="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;For a generic client or testing: the MCP Inspector from Anthropic runs tool calls through a web UI without configuring Claude Desktop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector node /absolute/path/to/build/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Inspector UI at port 6274 and you can fire tool calls manually and inspect the raw JSON-RPC traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Transport choice: stdio vs HTTP
&lt;/h2&gt;

&lt;p&gt;The setup above uses stdio transport. The client starts your server as a child process and communicates over stdin/stdout. This works for local tools and is the path of least resistance for Claude Desktop and Cursor.&lt;/p&gt;

&lt;p&gt;For a remote server that two or more clients share, you need HTTP transport. The SDK ships &lt;code&gt;StreamableHttpServerTransport&lt;/code&gt; for this. You pair it with an HTTP framework (Hono, Express, Fastify) and handle sessions. That setup adds meaningful complexity and is worth a separate article. Start with stdio unless you are building a shared service from day one.&lt;/p&gt;

&lt;p&gt;One rule that applies to both: never write to stdout with &lt;code&gt;console.log&lt;/code&gt; in a stdio server. The MCP protocol uses stdout for JSON-RPC frames. A stray log line corrupts the framing and the client sees a parse error with no helpful message. Use &lt;code&gt;console.error()&lt;/code&gt; for debugging output. Everything sent to stderr is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Common pitfalls
&lt;/h2&gt;

&lt;p&gt;The three mistakes I see in every first MCP server attempt:&lt;/p&gt;

&lt;p&gt;Schema validation gaps break calls silently. If the model sends an input that does not match your Zod schema, the SDK rejects it with a generic error. The model may retry with the same bad input. Write the schema narrowly and add &lt;code&gt;.describe()&lt;/code&gt; calls on each field to help the model understand what values are valid.&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;// add field-level descriptions so the model knows what to send&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^#&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-fA-F&lt;/span&gt;&lt;span class="se"&gt;]{6}&lt;/span&gt;&lt;span class="sr"&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;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Six-digit hex color with leading #, e.g. #ff5733&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;Error responses need the right shape. When your tool handler throws, return a structured error instead of letting the exception propagate:&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;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hex&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;try&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... rest of handler&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;catch &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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&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="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;isError&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="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;The &lt;code&gt;isError: true&lt;/code&gt; flag tells the client the call failed, which surfaces properly in Claude Desktop rather than showing as a successful response with error text inside.&lt;/p&gt;

&lt;p&gt;Resource URIs must be stable. If a client caches a resource URI and your server changes it on restart, the cached reference points nowhere. Treat resource URIs like public API paths: change them only when you intend a breaking change and version them if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;MCP is not a new protocol that requires learning a whole ecosystem. The SDK is thin. You write a handler function, attach a schema, return a content array. The hard part is designing the right tools: narrow enough to be reliable, broad enough to be useful. A tool that does one thing with a clear input schema outperforms a general-purpose tool with six optional fields every time.&lt;/p&gt;

&lt;p&gt;Build the color tool above. Get it running in Claude Desktop. Then replace the hex conversion with whatever data or action you actually want to expose. The scaffolding is identical regardless of what the tool does.&lt;/p&gt;

&lt;p&gt;What would you expose through an MCP server if you had it running today?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The scaffolding is 30 minutes; the tool design is the actual work.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cursor 3 ships parallel AI agents. Here is the multi-agent workflow that actually works.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Tue, 26 May 2026 02:51:45 +0000</pubDate>
      <link>https://dev.to/thegdsks/cursor-3-ships-parallel-ai-agents-here-is-the-multi-agent-workflow-that-actually-works-2bk8</link>
      <guid>https://dev.to/thegdsks/cursor-3-ships-parallel-ai-agents-here-is-the-multi-agent-workflow-that-actually-works-2bk8</guid>
      <description>&lt;h1&gt;
  
  
  Cursor 3 ships parallel AI agents. Here is the multi-agent workflow that actually works.
&lt;/h1&gt;

&lt;p&gt;On April 2, 2026, Cursor shipped version 3.0 and called it "a unified workspace for building software with agents." The headline feature is the Agents Window: a sidebar that shows every active agent session, local or cloud, across all your repos, all at once.&lt;/p&gt;

&lt;p&gt;I have spent the past three weeks running it on a real codebase and the experience is different enough from any previous AI coding tool that it warrants a proper walkthrough. Not a demo. The actual workflow, with the parts that break.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;When you reach for it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agents Window&lt;/td&gt;
&lt;td&gt;Sidebar listing all active agent sessions&lt;/td&gt;
&lt;td&gt;Any time you run more than one agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local agents&lt;/td&gt;
&lt;td&gt;Composer 2 model, run in your open workspace&lt;/td&gt;
&lt;td&gt;Fast iteration, short-horizon tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud agents&lt;/td&gt;
&lt;td&gt;Runs offline, persists when laptop closes&lt;/td&gt;
&lt;td&gt;Long tasks, overnight runs, heavy refactors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local to cloud handoff&lt;/td&gt;
&lt;td&gt;Move a session between targets mid-task&lt;/td&gt;
&lt;td&gt;When a quick task grows into a long one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor Marketplace&lt;/td&gt;
&lt;td&gt;Plugins, MCPs, subagents, skills&lt;/td&gt;
&lt;td&gt;Extending what any agent can reach&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. What the Agents Window actually is
&lt;/h2&gt;

&lt;p&gt;Before Cursor 3, you had one agent session per window. You could open more than one Cursor window, but there was no unified view across them. The Agents Window fixes that by collecting all active sessions into a single sidebar panel.&lt;/p&gt;

&lt;p&gt;Open it with &lt;code&gt;Cmd+Shift+P&lt;/code&gt; and search "Agents Window". What you get is a list of every agent currently running: the task that started it, the repo it targets, and whether it runs locally or in the cloud. You can click into any session, see its chat history and file diffs, and redirect it.&lt;/p&gt;

&lt;p&gt;The practical change is visibility. Running three agents in parallel used to mean three browser tabs and a lot of alt-tabbing. Now you get one panel with three rows.&lt;/p&gt;

&lt;p&gt;What it does not do: it does not merge agent output automatically, it does not prevent two agents from writing to the same file, and it does not enforce any ordering between sessions. That coordination is still your job. Which is exactly why you need a workflow, not just the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The two execution targets and when to use each
&lt;/h2&gt;

&lt;p&gt;Cursor 3 ships with two places an agent can run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local agents
&lt;/h3&gt;

&lt;p&gt;A local agent runs in your open workspace using the Composer 2 model. It has access to your file system, your terminal, and your LSP (Language Server Protocol). When you ask it to refactor a function, it reads the file, writes the change, and you see the diff immediately. Round trip from prompt to edit runs in 5 to 15 seconds for most tasks.&lt;/p&gt;

&lt;p&gt;Use local agents when the task has a short time horizon, when you want to watch the work happen in real time, or when the task touches files that you are also actively editing. The Composer 2 model is fast, and the model that knows your workspace state best because it has direct file access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud agents
&lt;/h3&gt;

&lt;p&gt;A cloud agent runs on Cursor's infrastructure. The job persists even when your laptop closes. You can queue a long refactor, shut the lid, and come back four hours later to a PR ready for review. Cloud agents generate screenshots and demo recordings of the result so you can verify before you merge.&lt;/p&gt;

&lt;p&gt;Use cloud agents when the task will take longer than you want to babysit it, when you are working across more than one repository, or when you are running automations triggered from Slack, GitHub, or Linear. The Cursor Marketplace also ships subagent plugins specifically designed to extend cloud agent capabilities with external tool access.&lt;/p&gt;

&lt;p&gt;The handoff between local and cloud goes both ways. Start something locally, realize the scope expanded, hand it to cloud. Or pull a cloud result back into a local session to do final cleanup with LSP context.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A worked example: refactor pipeline split across 3 agents
&lt;/h2&gt;

&lt;p&gt;Here is the actual split I ran last week on a service that needed its logging replaced with structured JSON, its error handling standardized, and its test coverage filled in. Three distinct jobs with almost no overlap in the files they touched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a worktree for each agent to avoid branch conflicts&lt;/span&gt;
git worktree add ../refactor-logging feature/structured-logging
git worktree add ../refactor-errors feature/error-handling
git worktree add ../refactor-tests feature/test-coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git worktrees give each agent its own working directory on a separate branch. The agents are not sharing a working tree, so there are no write conflicts at the file level. The Agents Window still shows all three in the same sidebar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt structure
&lt;/h3&gt;

&lt;p&gt;Each agent gets a scoped prompt. The logging agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refactor all console.log and console.error calls in src/services/
to use the structured logger at src/lib/logger.ts. Output must be
JSON with fields: level, message, context. Do not change function
signatures. Do not touch test files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standardize all try/catch blocks in src/services/ to use the
AppError class in src/errors/app-error.ts. Rethrow with the
original error as the cause property. Do not change logging calls.
Do not touch test files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add missing unit tests for src/services/ using Vitest.
Cover the three exported functions with the lowest coverage
per the attached lcov.info. Do not edit source files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint "do not touch test files" in the first two prompts is not optional. Without it, agents drift toward touching shared files and you end up with three agents that all think they own &lt;code&gt;src/lib/logger.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring in the Agents Window
&lt;/h3&gt;

&lt;p&gt;With all three agents running, the Agents Window shows each session's current file and last action. You are not watching them run; you check back every 10 minutes to see if any of them has gone quiet or made a choice that looks wrong.&lt;/p&gt;

&lt;p&gt;The most common failure mode: an agent finishes one subtask and then starts making "improvements" to adjacent files outside its scope. Catch this early. The diff view inside each session tab shows you exactly what files the agent has queued for commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merging the results
&lt;/h3&gt;

&lt;p&gt;Each agent runs on its own branch. When all three finish, the merge sequence matters. Logging changes first, since error handling depends on the logger being correct. Error handling second. Tests third, because they exercise both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git merge feature/structured-logging
git merge feature/error-handling
git merge feature/test-coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the test suite after each merge, not just after the last one. If the test merge fails, you want to know which of the two prior merges introduced the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The orchestration gotchas
&lt;/h2&gt;

&lt;p&gt;Parallel agents are faster than sequential agents on tasks that do not share state. But they introduce three categories of failure that a single agent session avoids.&lt;/p&gt;

&lt;h3&gt;
  
  
  File conflicts
&lt;/h3&gt;

&lt;p&gt;Two agents writing to the same file at the same time produce a merge conflict that neither of them knows about. The only reliable prevention is prompt scoping. Give each agent an explicit list of directories it owns and an explicit list it must not touch. Worktrees help at the file system level, but they do not prevent two agents from editing the same path in different branches.&lt;/p&gt;

&lt;p&gt;If you skip this and end up with conflicts, do not ask a third agent to resolve them. Resolve merge conflicts manually. The context an agent needs to resolve a three-way conflict correctly is usually larger than what fits in a useful prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch divergence
&lt;/h3&gt;

&lt;p&gt;Agents that run long enough start diverging from main in ways that require manual rebase. A 4-hour cloud agent job started on Monday morning may return to a main branch that has 12 commits it did not see. Budget time for rebase before merge, especially on active repos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before merging any agent branch, rebase it&lt;/span&gt;
git checkout feature/structured-logging
git rebase main
&lt;span class="c"&gt;# resolve conflicts, then merge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cost ceiling
&lt;/h3&gt;

&lt;p&gt;Three agents running in parallel burn tokens three times as fast as one. Local agents use your Cursor subscription allocation. Cursor bills cloud agents separately for compute time, though no per-minute rate appears in the public docs at time of writing. Set a scope that finishes in under two hours for each agent on the first run. You will learn the actual token and time cost from those runs and can calibrate longer jobs after.&lt;/p&gt;

&lt;p&gt;The Agents Window does not have a built-in cost display per session at version 3.4. You get total usage in account settings. If you need per-session cost visibility, log the task start time and check account usage after the session ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The Agents Window is not magic. Treat it as a coordination surface for parallel work that you still have to design. The rule that made this actually work for me: treat each agent like a pull request reviewer who will only read the files you hand them. Scope, branch, scope again, then run.&lt;/p&gt;

&lt;p&gt;The real gain is not speed on one task. The gain is that three independent jobs that used to take three sequential afternoons now take one. The orchestration tax is real, but it pays back at 3x velocity on the right class of work.&lt;/p&gt;

&lt;p&gt;What kind of tasks are you splitting across agents? The comment thread from the first 90 minutes usually surfaces approaches I have not tried. Drop yours below.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Parallel agents are faster only when you design the seams between them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Microsoft tried to kill the printer driver. Healthcare said no.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Sat, 23 May 2026 06:36:49 +0000</pubDate>
      <link>https://dev.to/thegdsks/microsoft-tried-to-kill-the-printer-driver-healthcare-said-no-28e7</link>
      <guid>https://dev.to/thegdsks/microsoft-tried-to-kill-the-printer-driver-healthcare-said-no-28e7</guid>
      <description>&lt;h1&gt;
  
  
  Microsoft tried to kill the printer driver. 90% of US healthcare said no.
&lt;/h1&gt;

&lt;p&gt;In late 2025, Microsoft put a line on the Windows Roadmap that should have read as routine. Starting January 2026, Windows Update would stop shipping legacy V3 and V4 printer drivers. Modern Print Platform only. Goodbye to a decade of brittle vendor blobs.&lt;/p&gt;

&lt;p&gt;In February 2026 they quietly took it back. The line vanished from the roadmap. The official statement told users no action applies. Existing printers will keep working. The deprecation, for now, sits on hold.&lt;/p&gt;

&lt;p&gt;Microsoft holds more market power than almost any company in history. They tried to retire a category of driver that Microsoft itself deprecated back in September 2023. They could not actually pull it off. The reason sits in every hospital in the United States, and it makes a noise like a 1990s modem.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V3 and V4 printer drivers&lt;/td&gt;
&lt;td&gt;Deprecated since September 2023, still alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;January 2026 deprecation push&lt;/td&gt;
&lt;td&gt;Announced, then retracted in February 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US healthcare communication that still runs on fax&lt;/td&gt;
&lt;td&gt;About 70 percent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Once you count EHR linked faxing&lt;/td&gt;
&lt;td&gt;Closer to 90 percent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ATM transactions still running on COBOL&lt;/td&gt;
&lt;td&gt;About 95 percent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Online banking transactions touching COBOL&lt;/td&gt;
&lt;td&gt;More than 40 percent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time horizon on this stuff actually dying&lt;/td&gt;
&lt;td&gt;Decades, not quarters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. The headline that almost happened
&lt;/h2&gt;

&lt;p&gt;The original Microsoft plan looked clean. V3 and V4 driver models carried known security and stability problems. Modern Print Platform, the IPP based replacement, outperforms them in almost every measurable way. Microsoft already deprecated the old drivers two and a half years ago. The January 2026 update would have completed the cleanup.&lt;/p&gt;

&lt;p&gt;That plan sits in the archive now. Tom's Hardware and Windows Central covered the original announcement. The retraction came after Microsoft "received feedback." The polite version of "received feedback" reads as follows: some quite large customers told Microsoft, in writing, that breaking the printer pipeline would break the hospital pipeline, and that the hospital pipeline runs on fax.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The fax number you cannot believe
&lt;/h2&gt;

&lt;p&gt;Here is the statistic that broke my brain when I first read it. Roughly 70 percent of healthcare communication in the United States still moves over fax. When you include EHR linked faxing, where an electronic health record system pretends to be a fax machine in order to talk to the rest of the industry, the number climbs to about 90 percent.&lt;/p&gt;

&lt;p&gt;Ninety percent. Of the most regulated, most digitized, most money-flooded industry in the developed world. Running on a protocol that predates the personal computer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   The 2026 healthcare comms diagram

  ┌──────────────┐         FAX           ┌──────────────┐
  │   Hospital A │  ─────────────────▶   │   Clinic B   │
  │   (modern    │                       │   (modern    │
  │    EHR)      │                       │    EHR)      │
  └──────────────┘                       └──────────────┘
        │                                       │
        ▼                                       ▼
   Pretends to be                          Pretends to be
   a fax machine                           a fax machine
        │                                       │
        ▼                                       ▼
  ╔═════════════════════════════════════════════════════╗
  ║   90% of the actual traffic goes over fax anyway    ║
  ╚═════════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That diagram explains what Microsoft hit when they tried to ship the driver change. The driver path covers more than home offices. The driver path runs through compliance pipelines that no single engineering team owns. Break the driver layer in January, and somebody's referral cannot reach somebody else's prior authorization in February. That outcome does not fit a "we will respond to feedback" narrative. That outcome makes a 60 Minutes segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The other infrastructure that refuses to die
&lt;/h2&gt;

&lt;p&gt;Fax counts as the most visible example. Not the only one. The pattern shows up everywhere stable infrastructure built up decades of edge cases. IBM has said for years, in slightly louder volumes each year, that COBOL still runs about 95 percent of ATM transactions and more than 40 percent of online banking. The COBOL workforce is aging out. The replacements never arrived. The systems keep running.&lt;/p&gt;

&lt;p&gt;Same pattern with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Year designed&lt;/th&gt;
&lt;th&gt;Still doing real work in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fax&lt;/td&gt;
&lt;td&gt;1843 (concept), 1960s mainstream&lt;/td&gt;
&lt;td&gt;Yes, in healthcare and government&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COBOL&lt;/td&gt;
&lt;td&gt;1959&lt;/td&gt;
&lt;td&gt;Yes, in banks and insurance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FORTRAN&lt;/td&gt;
&lt;td&gt;1957&lt;/td&gt;
&lt;td&gt;Yes, in scientific computing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;1974&lt;/td&gt;
&lt;td&gt;Yes, almost everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email (SMTP)&lt;/td&gt;
&lt;td&gt;1982&lt;/td&gt;
&lt;td&gt;Yes, the protocol you read every day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;1991&lt;/td&gt;
&lt;td&gt;Yes, you are reading this over it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We tell each other we live in a world of rapid change. The world actually sits on one of the most stable substrates the species has ever built. The application layer churns. The substrate hardly moves at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The lesson for software you ship today
&lt;/h2&gt;

&lt;p&gt;You will not build fax machines. You will, almost certainly, write code that outlives your current job, your current company, and possibly your current career. That outcome sits at the heart of the COBOL story that nobody puts on a slide. The COBOL devs in 1985 did not know their code would still run in 2026. They just shipped.&lt;/p&gt;

&lt;p&gt;The code you wrote last week might still serve as a production database adapter in 2040. The defaults you picked stand a chance of becoming invariants for some future maintainer who has never met you. Five practical rules that pay back over the decade-scale arc of code:&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1: Comment the boundary, not the line
&lt;/h3&gt;

&lt;p&gt;Your future maintainer can read your code. They cannot read your decision tree. Write down why a particular flag exists, why a particular workaround sits where it does, why a particular value lives as a constant. Skip the obvious. Document the negotiations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# bad
&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt;

&lt;span class="c1"&gt;# good
# Set to 47 seconds because the partner auth gateway has a hard 50s limit
# and we observed 1-2s of jitter from our load balancer in the May 2023
# postmortem. Do not raise without coordinating with the integrations team.
&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bad comment captures what the code already says. The good comment captures the negotiation that produced the number, which is the part that erases first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: Pick formats that read in plain text
&lt;/h3&gt;

&lt;p&gt;JSON, CSV, plain SQL, basic English logs. The dependency on a binary format with proprietary tooling bites archaeologists hardest. If somebody can &lt;code&gt;cat&lt;/code&gt; the file in 2046 and start guessing what it does, you have done them a favor that pays back forever.&lt;/p&gt;

&lt;p&gt;The fax format is plain enough that a forensic analyst can read it with the right hardware. COBOL source is plain enough that a junior dev with a manual can read it. The systems that died fastest in the 1990s and 2000s were the ones that depended on a binary tool that the vendor stopped supporting. Choose against that future.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Write the migration script you wish someone had written for you
&lt;/h3&gt;

&lt;p&gt;Every meaningful schema change should ship with the SQL or code that undoes it, or that walks the data from the old shape to the new one. Future you, or future someone, will thank you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Forward migration&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;preferred_locale&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'en-US'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;preferred_locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'en-GB'&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AU'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'NZ'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Down migration (commit this in the same file)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;preferred_locale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools like Alembic, Flyway, Liquibase, and Sequelize migrations enforce this discipline. If your team is doing migrations as ad-hoc DBAs running scripts in pgAdmin, you are storing technical debt that compounds at the rate of every release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4: Version your wire formats from day one
&lt;/h3&gt;

&lt;p&gt;The number one source of unkillable legacy infrastructure is a public protocol that grew without a version field. The 1843 fax protocol gained version negotiation only when CCITT standardized it. The internet has 30 years of bolt-on versioning because TCP/IP shipped without it. Avoid being the contributor of the next one.&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;good&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;API&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;everywhere&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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="s2"&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;Use date-based versioning, header-based versioning, or URL-based versioning. Pick one. Use it consistently. When you need to make a breaking change in five years, the version field is the only thing that lets you do it without breaking every client at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 5: Write a CHANGELOG that survives the company
&lt;/h3&gt;

&lt;p&gt;CHANGELOG.md, in the root of every repo you own. One entry per release. Date, version, and a sentence per change. Not generated. Written by a human. The future maintainer reads this before they read your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## [2026-05-12] - 2.4.1&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fixed billing rounding bug where orders with &amp;gt;100 line items
  rounded the tax down by 1 cent. See incident 2026-05-09.
&lt;span class="p"&gt;-&lt;/span&gt; Raised the partner gateway timeout from 30s to 47s. Coordinated with
  the integrations team. Do not raise further.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CHANGELOG is the only document that gets read in 2040. Make it count.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A short tour of the substrate you depend on right now
&lt;/h2&gt;

&lt;p&gt;If you think your stack is modern, the following table is for you. The right column is the year the underlying protocol or format reached its current dominant form. Every one of these things runs in the path of the request that loaded this article.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Protocol or format&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;TCP/IP&lt;/td&gt;
&lt;td&gt;1981&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain name&lt;/td&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;1983&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email transport&lt;/td&gt;
&lt;td&gt;SMTP&lt;/td&gt;
&lt;td&gt;1982&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email reading&lt;/td&gt;
&lt;td&gt;IMAP&lt;/td&gt;
&lt;td&gt;1986&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web transport&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;td&gt;1997&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time format&lt;/td&gt;
&lt;td&gt;Unix epoch&lt;/td&gt;
&lt;td&gt;1970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text encoding&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;td&gt;1993&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image format&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;1992&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image format&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;1996&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video format&lt;/td&gt;
&lt;td&gt;H.264&lt;/td&gt;
&lt;td&gt;2003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database query language&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;1974&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source control&lt;/td&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container format&lt;/td&gt;
&lt;td&gt;Tar&lt;/td&gt;
&lt;td&gt;1979&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;td&gt;POSIX shell&lt;/td&gt;
&lt;td&gt;1989&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The newest thing on that list is H.264, and it is 23 years old. Everything else has been there longer than most of the people reading this article have been alive. The "modern stack" is a thin veneer of frameworks over a substrate that predates the personal computer in most cases.&lt;/p&gt;

&lt;p&gt;This is not bad news. It is the most stable substrate any creative discipline has ever had to work on. Painters change pigments every century. Architects change materials every generation. Software engineers work on a foundation that has been mostly stable for 40 years. That foundation is what makes everything we build possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The honest take
&lt;/h2&gt;

&lt;p&gt;A tempting story sits here that goes "legacy is bad and we should kill it." That story misses the picture. The legacy systems stayed around because they work. A hundred million transactions a day stress-tested them, in front of regulators who would happily fine the carrier that broke them. The new systems will, eventually, earn the same proof. They have not yet.&lt;/p&gt;

&lt;p&gt;The reasonable position lands at humility. We do not count as the first generation to write important software. We will not count as the last. The substrate predates us. The substrate will probably outlast us.&lt;/p&gt;

&lt;p&gt;In a strange way, that picture reassures rather than worries. Microsoft cannot delete the printer driver. The fax machine still rings in your hospital. The work matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;A driver deprecation that should have been routine got walked back because the substrate it sits on is older, weirder, and more important than the people deprecating it remembered. Healthcare runs on fax. Banking runs on COBOL. Your job, whatever you ship next, is going to land in someone's &lt;code&gt;legacy/&lt;/code&gt; directory eventually. Write it like the next person matters.&lt;/p&gt;

&lt;p&gt;Question for the comments: what is the oldest piece of infrastructure your job still depends on, and how surprised would your CTO be to learn it is in the critical path?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The most modern thing in your stack is the part that is about to be legacy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Google redesigned 13 Workspace icons last week. Here is where to grab the new SVGs.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Fri, 22 May 2026 07:07:47 +0000</pubDate>
      <link>https://dev.to/thegdsks/google-redesigned-13-workspace-icons-last-week-here-is-where-to-grab-the-new-svgs-bc0</link>
      <guid>https://dev.to/thegdsks/google-redesigned-13-workspace-icons-last-week-here-is-where-to-grab-the-new-svgs-bc0</guid>
      <description>&lt;p&gt;On May 18 Google started rolling out new gradient icons for thirteen of its Workspace apps. Gmail, Drive, Docs, Sheets, Slides, Calendar, Chat, Meet, Vids, Forms, Keep, Voice, and Tasks all got refreshed artwork on the web. The iOS and Android rollouts began this week.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://thesvg.org/category/google-2026" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthesvg.org%2Fog-image.png" height="437" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://thesvg.org/category/google-2026" rel="noopener noreferrer" class="c-link"&gt;
            Google 2026 SVG Icons - Free Download (14 icons) | theSVG
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Browse and download 14 Google 2026 SVG icons. Free for personal and commercial use. Copy as SVG, JSX, React component, or CDN link.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthesvg.org%2Ficon.svg" width="32" height="32"&gt;
          thesvg.org
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you build a SaaS dashboard with a "works with Google Workspace" row, or a marketing page that shows the Gmail icon next to your integration copy, you have a small problem. The icons in your codebase are now the old set, and most projects do not have a fast path to refresh them.&lt;/p&gt;

&lt;p&gt;Here is what changed, why icon updates take so long to land in OSS libraries, and how to grab the new Google 2026 SVGs today without waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apps redesigned&lt;/td&gt;
&lt;td&gt;13 (Gmail, Drive, Docs, Sheets, Slides, Calendar, Chat, Meet, Vids, Forms, Keep, Voice, Tasks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual direction&lt;/td&gt;
&lt;td&gt;Gradient style, more distinct shape and color per app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color rule change&lt;/td&gt;
&lt;td&gt;Dropped the "all four Google colors" mandate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gmail exception&lt;/td&gt;
&lt;td&gt;Still uses more than one color, the only one in the set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web rollout&lt;/td&gt;
&lt;td&gt;Mid-May 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile rollout&lt;/td&gt;
&lt;td&gt;Late May 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSS SVGs available at&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://thesvg.org/category/google-2026" rel="noopener noreferrer"&gt;thesvg.org/category/google-2026&lt;/a&gt;, free, no attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. What changed in the Google 2026 icon set
&lt;/h2&gt;

&lt;p&gt;The earlier Google Workspace icons followed a strict rule. Every product icon had to use all four Google colors, blue, red, yellow, and green. The result was a row of icons that all looked vaguely similar at small sizes. A user in the app launcher would scan a wall of red-blue-yellow-green squares and pause to read the label.&lt;/p&gt;

&lt;p&gt;The new direction drops that rule. Each app now leans on one or two dominant colors and a clearer shape, with a soft gradient finish. Gmail is the one holdout that still keeps more than one color, because the envelope is the recognizable shape and the colors are part of the brand identity.&lt;/p&gt;

&lt;p&gt;The icons are also larger inside the same containing box. Most apps no longer ship the rounded-square page background, so the symbol takes up the full visual area instead of floating inside a card.&lt;/p&gt;

&lt;p&gt;You can see the new Google 2026 icons in two places today, the app launcher in the top-right of any Google site, and the New Tab page in Chrome. Open either and you are already looking at the refreshed set, even if you have not touched any setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why icon refreshes take time to reach your project
&lt;/h2&gt;

&lt;p&gt;This is the part that bites a freelancer at 5pm on a Friday.&lt;/p&gt;

&lt;p&gt;When a major brand refreshes its mark, the icon does not appear in your bundle on its own. Someone has to source the original from the brand's media kit or extract it from the live site. Then optimize the path through SVGO. Then verify it renders the same on dark and light backgrounds. Then categorize, name, and ship.&lt;/p&gt;

&lt;p&gt;For a single brand refresh that touches one product, the cycle takes days to weeks depending on bandwidth. For thirteen apps in one rollout, multiply that. The OSS community absorbs brand refreshes one path file at a time, and most icon catalogs run on volunteer hours.&lt;/p&gt;

&lt;p&gt;You get the gap. The official Google sites already show the new icons. Your app still shows the old ones. To a user who keeps Gmail open in a tab next to your dashboard, this reads as "this dashboard is stale." The icons are a small detail. Small details are what users read as signals of how current a product is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6prilrp6mqqu9dssdoyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6prilrp6mqqu9dssdoyl.png" alt="Google Workspace 2026 gradient icons preview" width="790" height="1554"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3rtjmo7uiagj7kefmtrq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3rtjmo7uiagj7kefmtrq.png" alt="Gmail Drive Calendar 2026 logo SVG side by side" width="790" height="1554"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/glincker" rel="noopener noreferrer"&gt;
        glincker
      &lt;/a&gt; / &lt;a href="https://github.com/glincker/thesvg" rel="noopener noreferrer"&gt;
        thesvg
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      6,035+ brand SVG icons for developers. Tree-shakeable, typed, open source. npm i thesvg
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a href="https://thesvg.org" rel="nofollow noopener noreferrer"&gt;
    
      
      
      &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Flogo-wordmark-dark.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Flogo-wordmark-dark.svg" alt="theSVG" height="48"&gt;&lt;/a&gt;
    
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;strong&gt;6,030+ SVG icons. Brands, AWS, Azure, GCP, and more. Search, copy, ship.&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b4dc1bf1b42d2a80d5ccae55b0d1b369776182f58da72c63d6500be0a4472fa1/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f7468657376673f7374796c653d666c61742d73717561726526636f6c6f723d463937333136266c6162656c3d6e706d" alt="npm"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/eaf6bfbece55b4c168a95aded320f85b518f3a1430074427794ab2ca55852aec/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f7468657376673f7374796c653d666c61742d73717561726526636f6c6f723d463937333136266c6162656c3d646f776e6c6f616473" alt="downloads"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4f1e2c86fd38a85a4fb90a74ffc1bddf6e4d8aa7a40e63ffd886bb43ba6f115d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f676c696e636b65722f7468657376673f7374796c653d666c61742d737175617265266c6162656c3d7374617273" alt="stars"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3a89f40d533dba266a6343a36c2d6cd279a8371c62d25580c80618b49a8fc271/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f69636f6e732d362532433033302532422d4639373331363f7374796c653d666c61742d737175617265" alt="6,030+ icons"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/07929a65aba7429404604f02ee788a9f1351d9a03fef2af7a2cf1ebfcf88f0d7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f676c696e636b65722f7468657376673f7374796c653d666c61742d737175617265" alt="license"&gt;&lt;/a&gt;
  &lt;a href="https://www.figma.com/community/plugin/1612997159050367763" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4c1c8dadd07a324f1517b623e75393965e23553bbbf27df5b4f075cdf73ed3a9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4669676d612d506c7567696e2d4632344531453f7374796c653d666c61742d737175617265266c6f676f3d6669676d61266c6f676f436f6c6f723d7768697465" alt="Figma"&gt;&lt;/a&gt;
  &lt;a href="https://marketplace.visualstudio.com/items?itemName=glincker.thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6df9c2b04e7d77c4e119d1f0921876c6af49c8dd5ee4fe2c61c944392633bf8a/68747470733a2f2f696d672e736869656c64732e696f2f76697375616c2d73747564696f2d6d61726b6574706c6163652f762f676c696e636b65722e7468657376673f7374796c653d666c61742d73717561726526636f6c6f723d303037414343266c6162656c3d5653253230436f6465266c6f676f3d76697375616c73747564696f636f6465" alt="VS Code"&gt;&lt;/a&gt;
  &lt;a href="https://www.raycast.com/thegdsks/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/851575e7a28de939a29c99b66d1ce6bd4d4116a7c5d7d776439f2ca8d2c474ea/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f526179636173742d53746f72652d4646363336333f7374796c653d666c61742d737175617265266c6f676f3d72617963617374" alt="Raycast"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/tree/main/extensions/neovim" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/88938f8fb7c9b2530ed80bc1baa277b565e1d74e441915ed8fb5a0ffd7bb2cfc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4e656f76696d2d506c7567696e2d3031393733333f7374796c653d666c61742d737175617265266c6f676f3d6e656f76696d266c6f676f436f6c6f723d7768697465" alt="Neovim"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/tree/main/extensions/alfred" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f142a8f2a414349f30c767e1ed8629d1aec636b3158d12f57b95cc9678f87514/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f416c667265642d576f726b666c6f772d3543314638373f7374796c653d666c61742d737175617265266c6f676f3d616c66726564266c6f676f436f6c6f723d7768697465" alt="Alfred"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/thesvg/tree/main/extensions/browser" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4679826ee1f5e8ae42dffe17d237c19507ea305f888ab62eb7dec66b6fb841a4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4368726f6d652d436f6d696e67253230536f6f6e2d3432383546343f7374796c653d666c61742d737175617265266c6f676f3d676f6f676c656368726f6d65266c6f676f436f6c6f723d7768697465" alt="Chrome"&gt;&lt;/a&gt;
  &lt;a href="https://skills.sh/glincker/thesvg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7884786d99c662ebc92d270db565bdcea9b1fa73d9948695ad252f57b2ea5f22/68747470733a2f2f736b696c6c732e73682f622f676c696e636b65722f746865737667" alt="skills.sh"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/glincker/homebrew-thesvg" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7b5a3b756568109aaa99f492de96b126a5cb0268c68c643ecc33541769d7780d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f486f6d65627265772d7468657376672d4642423034303f7374796c653d666c61742d737175617265266c6f676f3d686f6d6562726577266c6f676f436f6c6f723d7768697465" alt="Homebrew"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://thesvg.org" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Browse Icons&lt;/strong&gt;&lt;/a&gt;  • 
  &lt;a href="https://github.com/glincker/thesvg#install" rel="noopener noreferrer"&gt;Install&lt;/a&gt;  • 
  &lt;a href="https://github.com/glincker/thesvg#extensions" rel="noopener noreferrer"&gt;Extensions&lt;/a&gt;  • 
  &lt;a href="https://github.com/glincker/thesvg#cdn" rel="noopener noreferrer"&gt;CDN&lt;/a&gt;  • 
  &lt;a href="https://github.com/glincker/thesvg#api" rel="noopener noreferrer"&gt;API&lt;/a&gt;  • 
  &lt;a href="https://github.com/glincker/thesvg#packages" rel="noopener noreferrer"&gt;Packages&lt;/a&gt;  • 
  &lt;a href="https://thesvg.org/compare" rel="nofollow noopener noreferrer"&gt;Compare&lt;/a&gt;  • 
  &lt;a href="https://github.com/glincker/thesvg#contributing" rel="noopener noreferrer"&gt;Contribute&lt;/a&gt;
&lt;/p&gt;



&lt;p&gt;
  &lt;a href="https://thesvg.org" rel="nofollow noopener noreferrer"&gt;
    &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fglincker%2Fthesvg%2Fmain%2Fpublic%2Fog-image.png" alt="theSVG - 6,030+ SVG icons for developers" width="720"&gt;
  &lt;/a&gt;
&lt;/p&gt;



&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why theSVG?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Most icon libraries focus on UI icons. Brand logos are scattered across press kits, Figma files, and random GitHub repos. &lt;strong&gt;theSVG&lt;/strong&gt; is the single source for SVG icons - brand logos, cloud architecture diagrams, and more. Searchable, versioned, and available as npm packages, CDN, CLI, API, and MCP server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6,030+ icons&lt;/strong&gt; across multiple collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4,019 brand icons&lt;/strong&gt; across 55+ categories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;739 AWS Architecture icons&lt;/strong&gt; (2026-Q1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;626 Azure Service icons&lt;/strong&gt; (2026-Q1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;214 Google Cloud icons&lt;/strong&gt; (2026-Q1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8,400+ SVG variants&lt;/strong&gt; - color, mono, light, dark, wordmark&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tree-shakeable&lt;/strong&gt; - import one icon, ship only that icon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript-first&lt;/strong&gt; - fully typed, dual ESM/CJS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework-agnostic&lt;/strong&gt; - React, Vue, Svelte, plain HTML, or CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-ready&lt;/strong&gt; - MCP server for Claude, Cursor, and Windsurf&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Collections&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;theSVG organizes…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/glincker/thesvg" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  3. Where to grab the Google 2026 SVGs today
&lt;/h2&gt;

&lt;p&gt;The full Google 2026 icon set is live in the open-source library &lt;a href="https://thesvg.org/category/google-2026" rel="noopener noreferrer"&gt;thesvg.org&lt;/a&gt;. All thirteen Workspace apps are in the catalog with the new gradient artwork, shipped the same week as Google's web rollout. License: free, no attribution required. The repo is on GitHub at &lt;a href="https://github.com/GLINCKER/thesvg" rel="noopener noreferrer"&gt;GLINCKER/thesvg&lt;/a&gt; if you want to contribute, file an issue, or fork.&lt;/p&gt;

&lt;p&gt;Install via npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;thesvg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download direct from the site. URLs follow a stable pattern, &lt;code&gt;/icons/[brand]/[variant].svg&lt;/code&gt;, so you can wire them into a build step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/components/GoogleIcon.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// Server component or build-time loader, not a runtime fetch in production&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;readFileSync&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;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;join&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;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;IconName&lt;/span&gt; &lt;span class="o"&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;gmail&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;google-drive&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;google-docs&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;google-sheets&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;google-slides&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;google-calendar&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;google-chat&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;google-meet&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;google-vids&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;google-forms&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;google-keep&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;google-voice&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;google-tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GoogleIcon&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="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IconName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&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;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;join&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;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public/icons&lt;/span&gt;&lt;span class="dl"&gt;'&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;2026.svg&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;utf-8&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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;For a Vite or Next.js project, the cleaner path is to import the SVG as a component through your bundler's SVG loader. The above is the read-the-file version for projects that do not have a loader configured yet.&lt;/p&gt;

&lt;p&gt;If you maintain an OSS app and need to migrate to the Google 2026 icons fast for a release this week, the path is: install the package, swap your existing Google icon imports for the 2026 variants, handle the Gmail edge case below, ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Gmail multi-color edge case
&lt;/h2&gt;

&lt;p&gt;One thing worth handling carefully in your render code. Gmail is the only app in the new Google 2026 set that keeps more than one color. The other twelve work fine with a &lt;code&gt;currentColor&lt;/code&gt; fill or a single-color CSS override. Gmail breaks if you do that, because the multi-color fill is the brand.&lt;/p&gt;

&lt;p&gt;If your design system applies a &lt;code&gt;color&lt;/code&gt; prop to all logos uniformly, you need a special case for Gmail, or you ship two render paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BrandIcon&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="nx"&gt;color&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IconName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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;preservesColor&lt;/span&gt; &lt;span class="o"&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;gmail&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;preservesColor&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleIcon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleIcon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;This is the kind of edge case the old four-color rule used to hide. When every icon used four colors, you knew you could not apply a single-color override to any of them. Now twelve out of thirteen work fine with an override and one does not. Read your design system docs accordingly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F14cyz28zml6xh5f7y7gz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F14cyz28zml6xh5f7y7gz.png" alt="Gmail 2026 multi-color SVG render example" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The bigger pattern
&lt;/h2&gt;

&lt;p&gt;Brand refreshes ship faster than the icon ecosystem can absorb them. This is the third major refresh of the past two years where the official site updates on day zero and the broader OSS catalog catches up over weeks. When you depend on a third-party library to ship brand assets, you are accepting a built-in lag.&lt;/p&gt;

&lt;p&gt;The fix is not to abandon icon libraries. The fix is to know which catalogs already have the assets you need for the release you are shipping this week, and to pick accordingly. For a marketing page going live now with a "works with Google" row, you want the catalog that already has the Google 2026 set. For a long-running design system, the audit trail and naming convention matter more than speed.&lt;/p&gt;

&lt;p&gt;The OSS community is at its best when a new resource lands and people share it before everyone has to rebuild it from scratch. That is the spirit here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzsaus4rn7fapfmktb14m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzsaus4rn7fapfmktb14m.png" alt="Google 2026 icons SVG download from thesvg.org" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Google shipped new gradient icons for thirteen Workspace apps on May 18. The web rollout is live, the mobile rollout is in progress, and the new SVGs are already available as OSS at &lt;a href="https://thesvg.org/category/google-2026" rel="noopener noreferrer"&gt;thesvg.org/category/google-2026&lt;/a&gt;, free with no attribution. If you build product that lives next to Workspace in your users' tabs, the migration takes one afternoon.&lt;/p&gt;

&lt;p&gt;What does your icon-refresh workflow look like when a major brand drops a redesign overnight? Drop a comment with your current setup.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · building &lt;a href="https://thesvg.org" rel="noopener noreferrer"&gt;thesvg.org&lt;/a&gt; and &lt;a href="https://glincker.com" rel="noopener noreferrer"&gt;Glincker&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Brand refreshes are the moment your icon library reveals whether it is curated or just convenient.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>design</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I shipped a working landing page in 14 KB. Here is every byte.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Thu, 21 May 2026 02:06:58 +0000</pubDate>
      <link>https://dev.to/thegdsks/i-shipped-a-working-landing-page-in-14-kb-here-is-every-byte-8p3</link>
      <guid>https://dev.to/thegdsks/i-shipped-a-working-landing-page-in-14-kb-here-is-every-byte-8p3</guid>
      <description>&lt;h1&gt;
  
  
  I shipped a working landing page in 14 KB. Here is every byte.
&lt;/h1&gt;

&lt;p&gt;In May 2026 a coder who goes by Monster placed fourth at the Speccy.pl demoparty with a working 256-byte ZX Spectrum intro. Two hundred and fifty six bytes. The whole program is shorter than the tweet announcing a Series A. Meanwhile the median web page in the 2025 HTTP Archive Web Almanac weighs 2,617 KB on desktop and 2,452 KB on mobile. The 2026 web page is the same size as a 1996 SimCity install, minus the cities, plus a cookie banner.&lt;/p&gt;

&lt;p&gt;I wanted to know what the floor actually is for a usable modern landing page. Not a demo trick. Not assembly. A real page with a headline, a value prop, three feature blocks, a form, a footer, and analytics. Production grade copy, accessible markup, decent typography. What is the smallest you can ship that without losing anything that actually matters?&lt;/p&gt;

&lt;p&gt;The honest answer turned out to be 14 KB, total, over the wire. That is one TCP slow-start window. The page renders in under 50 milliseconds on a midrange Android. The audit was instructive enough that I want to walk through it line by line.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Common size&lt;/th&gt;
&lt;th&gt;The 14 KB version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;30 to 80 KB&lt;/td&gt;
&lt;td&gt;4 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;80 to 300 KB&lt;/td&gt;
&lt;td&gt;3 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;400 to 2,000 KB&lt;/td&gt;
&lt;td&gt;0 KB (none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web fonts&lt;/td&gt;
&lt;td&gt;100 to 400 KB&lt;/td&gt;
&lt;td&gt;0 KB (system fonts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;500 to 3,000 KB&lt;/td&gt;
&lt;td&gt;6 KB (inline SVG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;50 to 200 KB&lt;/td&gt;
&lt;td&gt;1 KB (custom pixel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total over wire&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 to 6 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14 KB gzipped&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The methodology and the file follow. Everything is reproducible. No magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The 14 KB number is not arbitrary
&lt;/h2&gt;

&lt;p&gt;There is a deeply nerdy reason to target 14 KB specifically. TCP slow start. When a browser opens a connection, the server is allowed to send roughly ten packets in the first round trip before waiting for an acknowledgement. Ten packets, each about 1,460 bytes after headers, gives you the famous "first 14 KB" window.&lt;/p&gt;

&lt;p&gt;If your entire above-the-fold critical path fits in those 14 KB, the browser can render meaningful content in one round trip. If it does not, you pay another RTT for every additional 14 KB chunk. On a 100 ms latency mobile connection, three round trips is the difference between 100 ms and 400 ms to first paint, which is the difference between "the page is fast" and "the page is loading."&lt;/p&gt;

&lt;p&gt;You will see "14 KB rule" floated as folklore. The math is real. Google's web.dev has the canonical writeup, the Chrome devrel team uses the same number in their performance teaching materials, and the HTTP Archive's annual report references it explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Where the bytes go in a typical landing page
&lt;/h2&gt;

&lt;p&gt;Before you can cut bytes, you need to know where they are. The breakdown for an average 2026 marketing page, in my measurements across a few dozen popular landing pages, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Layer            | Median KB | Share of total
   ─────────────────┼──────────┼───────────────
   Images           |  1,400   |  54%
   JavaScript       |    580   |  22%
   Fonts            |    220   |   8%
   CSS              |    180   |   7%
   HTML             |     60   |   2%
   Video previews   |    140   |   5%
   Analytics + ads  |     60   |   2%
   ─────────────────┼──────────┼───────────────
   Total            |  2,640   | 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image and JavaScript layers are 76 percent of every landing page. Cut those two layers seriously and you cut the page weight by a factor of four without touching anything else. Cut them aggressively and you can hit the 14 KB target.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The HTML layer (target: 4 KB)
&lt;/h2&gt;

&lt;p&gt;The HTML is structural. It needs to be semantic enough that the page works with no CSS or JS, accessible enough to pass an audit, and short enough to fit in the budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Page Title That Tells The Truth&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"What this page is, in one sentence"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"data:image/svg+xml,&amp;lt;svg xmlns='...'/&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;&lt;span class="c"&gt;/* CSS inlined here, see next section */&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Brand&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/pricing"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pricing&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/docs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Docs&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;The single sentence that tells the reader why they are here.&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The follow up sentence with the value proposition.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/signup"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start free&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Three things that matter&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Thing one&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Why it matters in 14 words or less.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Thing two&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Why it matters in 14 words or less.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Thing three&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Why it matters in 14 words or less.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/signup"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Get started&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;small&amp;gt;&lt;/span&gt;copyright 2026 your name&lt;span class="nt"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That HTML is around 1.6 KB before CSS. Real production copy expands it, but you have plenty of headroom under the 4 KB target.&lt;/p&gt;

&lt;p&gt;Three things this HTML does not do, on purpose: no div soup, no class names on every element, no script tags. The CSS will target the semantic tags directly. The form submits to the server, no JavaScript handler. Progressive enhancement is the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The CSS layer (target: 3 KB)
&lt;/h2&gt;

&lt;p&gt;The trap in CSS is loading a framework. Tailwind in production is 8 to 40 KB depending on the purge config. Bootstrap is 25 KB minified. The 14 KB version uses no framework at all. Modern CSS makes this possible in a way it was not five years ago.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f172a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fafaf9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#16a34a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;64rem&lt;/span&gt;&lt;span class="p"&gt;;&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="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.6&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;margin-inline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;nav&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6vw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4rem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.cta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto-fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#cbd5e1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#64748b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f8fafc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f172a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e293b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#334155&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;That CSS is about 1.4 KB. It supports dark mode, responsive layout via CSS Grid auto-fit, fluid type via &lt;code&gt;clamp()&lt;/code&gt;, and accessible focus states (inherited from browser defaults, which are fine).&lt;/p&gt;

&lt;p&gt;Three CSS features doing heavy lifting that did not exist five years ago: &lt;code&gt;clamp()&lt;/code&gt; for fluid type, CSS Grid &lt;code&gt;auto-fit&lt;/code&gt; for responsive columns without media queries, and CSS custom properties for theming. All three landed in Baseline before 2023. Use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The JavaScript layer (target: 0 KB)
&lt;/h2&gt;

&lt;p&gt;For a marketing page, the right amount of JavaScript is none.&lt;/p&gt;

&lt;p&gt;Almost every interactivity pattern you needed JavaScript for in 2018 has a native equivalent in 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You needed JS for&lt;/th&gt;
&lt;th&gt;You can use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hamburger menu&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modal dialog&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; with &lt;code&gt;showModal()&lt;/code&gt; (or zero JS with a CSS popover)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooltip&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;title&lt;/code&gt; attribute or CSS &lt;code&gt;:hover&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form validation&lt;/td&gt;
&lt;td&gt;native &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;type=email&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smooth scroll&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scroll-behavior: smooth&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lazy load&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loading="lazy"&lt;/code&gt; on images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme toggle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carousel&lt;/td&gt;
&lt;td&gt;CSS scroll-snap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accordion&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The thing nobody mentions: a marketing page does not need a carousel. Most of those JavaScript "features" are noise. Cut them. Your page is faster, your bundle is smaller, and your reader sees the copy you wrote sooner.&lt;/p&gt;

&lt;p&gt;If you absolutely need a single interactive component, write the JavaScript inline. A useful button handler is under 200 bytes. A SPA framework is 200 KB. The ratio is 1000:1. You are paying for the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The image layer (target: 6 KB)
&lt;/h2&gt;

&lt;p&gt;The single biggest lever. Most landing pages use a hero photo, three feature illustrations, and a footer logo strip. Sometimes a video. All of it is unnecessary in 2026.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- inline SVG for a feature icon, ~200 bytes --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;
     &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M4 12l4 4 12-12"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An inline SVG checkmark is 200 bytes. An equivalent PNG is 3 KB. An equivalent stock icon font that ships 500 icons you do not use is 80 KB. Inline SVG wins every time.&lt;/p&gt;

&lt;p&gt;For hero imagery, the question is harder. Three answers depending on what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Option A: no hero image at all
  Pros: 0 KB, no decision fatigue, the copy carries the page
  Cons: looks "minimal," which some audiences read as "incomplete"

Option B: an inline CSS gradient or shape
  Pros: under 1 KB, scales to any screen, works on no connection
  Cons: not photographic

Option C: a single AVIF/WebP at the actual display size
  Pros: rich visual, the photo carries the story
  Cons: 30 to 200 KB even at the floor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the 14 KB target page I went with Option B. A CSS gradient and an SVG glyph. The result reads as deliberate and modern rather than empty.&lt;/p&gt;

&lt;p&gt;If you must ship a photo, the absolute floor for a hero image at 1200x630, AVIF, quality 50, is about 25 KB. That blows the 14 KB budget by itself. The math says you pick option A or B for the 14 KB page, and accept 30 KB total page weight as the floor when you need a real photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The analytics layer (target: 1 KB)
&lt;/h2&gt;

&lt;p&gt;You do not need Google Analytics. You do not need Mixpanel. You do not need Segment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ~80 bytes, fires once on page load, no cookies --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/p?u=/"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 1x1 pixel image with a query string captures the page view server-side. Your access logs already contain the rest of the information (referrer, user agent, IP for geo if you need it). For a marketing page, a server-side pixel is enough for 90 percent of teams.&lt;/p&gt;

&lt;p&gt;If you need event tracking, write the 200-byte fetch yourself:&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;track&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/e?n=&lt;/span&gt;&lt;span class="dl"&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="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;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keepalive&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire analytics SDK for a small site. 100 bytes minified. You do not need a 60 KB analytics library to count clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Putting it together
&lt;/h2&gt;

&lt;p&gt;The final file, including the prose, all CSS, all SVG, the analytics pixel, and a fake form action, lands at 14 KB on the wire after gzip. The breakdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Layer        | Pre-gzip | Post-gzip
   ─────────────┼─────────┼──────────
   HTML body    |   4.1 KB |   1.8 KB
   CSS (inline) |   2.9 KB |   1.3 KB
   SVG (inline) |   5.8 KB |   1.6 KB
   Analytics    |   0.6 KB |   0.4 KB
   HTTP headers |    n/a   |   0.4 KB
   Compression  |   1x     |   ~2.6x
   ─────────────┼─────────┼──────────
   Total on wire|         |  14.0 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page loads in 28 ms on a fiber connection, 180 ms on a throttled 3G connection. Lighthouse score: 100/100/100/100. No frameworks. No build step. One HTML file with inline CSS and SVG. The file is on my site, you can view source on it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. The honest take
&lt;/h2&gt;

&lt;p&gt;You are not going to ship every page at 14 KB. You should not try. A real product needs interactivity, real photos, real auth flows, real client state. Those things cost bytes legitimately.&lt;/p&gt;

&lt;p&gt;What you should ship at 14 KB or close to it: every marketing page, every documentation page, every "about" page, every blog post. The pages where the reader is reading prose and looking at a CTA. That category is most of your top of funnel. That category is where bundle size translates directly to conversion rate, because slow pages drive bounces.&lt;/p&gt;

&lt;p&gt;The demoscene has been asking "do we need this byte" for forty years. The rest of the industry forgot. The good news is that the muscle comes back fast. Once you ship one page at 14 KB you will start seeing your other pages the way Monster sees a ZX Spectrum: as a budget, not a blank check.&lt;/p&gt;

&lt;p&gt;Question for the comments: what is your current landing page weight, and what is the single byte-heavy thing you would cut first?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The bytes you never spent are the ones your users will thank you for, even if they never see them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The portfolio math. When 30 small apps beat 1 big one.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Tue, 19 May 2026 03:36:50 +0000</pubDate>
      <link>https://dev.to/thegdsks/the-portfolio-math-when-30-small-apps-beat-1-big-one-41ai</link>
      <guid>https://dev.to/thegdsks/the-portfolio-math-when-30-small-apps-beat-1-big-one-41ai</guid>
      <description>&lt;h1&gt;
  
  
  The portfolio math. When 30 small apps beat 1 big one.
&lt;/h1&gt;

&lt;p&gt;For a decade the indie hacker playbook stayed the same. Pick one product. Find a niche. Focus. Iterate. Sell. That advice fit 2014 perfectly. It started quietly going wrong in 2022, and by 2026 it is the wrong default for most solo operators, including a meaningful chunk of the ones the courses are still selling it to.&lt;/p&gt;

&lt;p&gt;Eight solo founders crossed twenty thousand dollars a month in revenue between November 2025 and April 2026. The shape of how they got there is not the shape the courses describe. The shape is a portfolio. One person, many products, lots of small bets, no precious single hill to die on.&lt;/p&gt;

&lt;p&gt;This article is the economic case for the portfolio shape, the math that determines whether it fits your situation, the kill rule that makes it work, and a working calculator you can paste into a spreadsheet this afternoon. By the end you will know whether you should be running one product or seven, and you will know exactly what number to track to decide if a given product belongs in the portfolio.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The choice&lt;/th&gt;
&lt;th&gt;When it wins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single product, all-in&lt;/td&gt;
&lt;td&gt;High build cost, defensible moat, large addressable market, slow feedback cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio of 5 to 10&lt;/td&gt;
&lt;td&gt;Medium build cost, fragmented attention, fast feedback cycles, you have any distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio of 20-plus&lt;/td&gt;
&lt;td&gt;Very low build cost, niche-of-niches, owned channel, willing to kill aggressively&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The math behind that table is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The case for the portfolio in three numbers
&lt;/h2&gt;

&lt;p&gt;The portfolio shape is not a fashion. It is a response to three measurable changes since 2014.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Number 1: build cost per product, in hours
  2014:  ~400 hours for a working SaaS with payments
  2020:  ~120 hours, same scope
  2026:   ~25 hours, same scope, including auth, payments, and a usable UI

Number 2: cost per useful signal, in product-attempts
  2014:  one attempt, run for 6 to 12 months, then maybe one more
  2026:  ten to thirty attempts, each run for 30 to 90 days

Number 3: average successful attempt rate, indie SaaS
  Published founder reports cluster around 1 in 8 to 1 in 15
  Conservative call: 1 in 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine those three. In 2014 you got one shot per year. In 2026 you can take twenty shots per year. If one in ten shots becomes a paying product, the single-shot strategy gets you to a paying product roughly every decade. The twenty-shot strategy gets you to two paying products per year, on average.&lt;/p&gt;

&lt;p&gt;This is the entire economic argument for the portfolio. It is not that portfolios are inherently better. It is that the cost of an attempt fell by an order of magnitude, and the strategy that matches the new cost is to take more attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The actual revenue distribution inside a portfolio
&lt;/h2&gt;

&lt;p&gt;The first thing to understand is that a portfolio does not produce uniform revenue. It produces a long tail.&lt;/p&gt;

&lt;p&gt;Max, one of the eight founders from the writeup, makes $22K MRR across thirty apps. Average revenue per app: $733. That number is misleading. The real distribution probably looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   App rank | Estimated share of MRR | Estimated MRR
   ─────────┼───────────────────────┼──────────────
   App 1    |  35 to 50%            |  $7,700 to $11,000
   App 2    |  15 to 20%            |  $3,300 to $4,400
   App 3    |  10 to 15%            |  $2,200 to $3,300
   App 4-6  |  5 to 8% each         |  $1,100 to $1,760 each
   App 7-15 |  1 to 3% each         |  $220 to $660 each
   App 16+  |  near zero            |  rounding error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distribution above is a power law, which is the same shape every portfolio of consumer or SMB SaaS products converges to. Pieter Levels has been transparent about this for years across his 12-plus product portfolio. A handful of products carry the revenue. The rest exist to feed the funnel and explore new niches.&lt;/p&gt;

&lt;p&gt;If you find this depressing, the portfolio shape is not for you. The right reading is liberating: you do not need every product to win. You need to ship enough that one or two find the power law top.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The kill rule is the load-bearing piece
&lt;/h2&gt;

&lt;p&gt;The thing that separates a working portfolio from a graveyard of half-finished SaaS projects is the kill rule. Without it the portfolio becomes a tax. Each product needs maintenance. Each product accumulates support tickets, dependency upgrades, expired domains, broken Stripe webhooks. A portfolio of unkilled losers will eat all your time.&lt;/p&gt;

&lt;p&gt;The kill rule has three components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Component 1: time horizon
  Pick a number, write it down, do not negotiate with yourself later.
  Reasonable defaults: 30 days for SaaS, 60 days for content products,
                       90 days for marketplaces.

Component 2: signal threshold
  Define the minimum signal that justifies keeping the product alive.
  Reasonable defaults: 3 paying customers, OR $50 MRR,
                       OR 100 active users with stickiness over 20%

Component 3: kill action
  Define exactly what "kill" means before you have to do it.
  Standard practice: archive the repo, sunset the domain,
                     refund any remaining subscribers, write the postmortem.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A working kill rule reads like a contract: "If this product has fewer than 3 paying customers 30 days after launch, I archive the repo, redirect the domain to my portfolio page, and write a one-page postmortem before starting the next product."&lt;/p&gt;

&lt;p&gt;The contract part matters. You will not want to kill the product. You will have spent 25 hours on it. You will have a tiny number of free users who like it. You will tell yourself that with one more feature it will take off. The kill rule is the version of you that wrote the contract overruling the version of you that is sentimental about the work.&lt;/p&gt;

&lt;p&gt;The portfolio founders who succeed are not the ones with the best products. They are the ones with the strictest kill rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A working calculator
&lt;/h2&gt;

&lt;p&gt;You can decide whether the portfolio shape fits your situation with this calculator. Paste it into a spreadsheet, fill in the inputs, read the recommendation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INPUTS

  H = hours to ship a working version (including auth, payments, UI)
  S = your success rate per attempt (default 0.10 if unknown)
  D = distribution multiplier (1.0 if launching cold, 3.0 if you have any
      owned channel, 8.0 if you have a list of 5K-plus engaged followers)
  W = available hours per week
  K = your sentimental kill tax (in extra hours per failed product)

CALCULATIONS

  Attempts per year possible:
    A = (W * 50) / (H + K)

  Expected successful products per year:
    P = A * S * D

  Cost per successful product:
    C = (H + K) / (S * D)

DECISION RULES

  If P &amp;lt; 1, you cannot run a portfolio. Pick a single product.
  If 1 &amp;lt;= P &amp;lt; 3, run a small portfolio of 5 products. Be strict.
  If P &amp;gt;= 3, run an aggressive portfolio. Kill faster.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example for a founder with 20 hours a week, 25-hour builds, no distribution, no kill tax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A = (20 * 50) / (25 + 0) = 40 attempts per year possible
P = 40 * 0.10 * 1.0 = 4 expected successes per year
C = 25 / (0.10 * 1.0) = 250 hours per successful product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That founder should run an aggressive portfolio. Same founder, same hours, but with a 10K-follower X account that they have nurtured for two years:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A = (20 * 50) / 25 = 40 attempts per year
P = 40 * 0.10 * 8.0 = 32 expected successes per year (clip to feasibility)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The math goes silly fast when distribution is the multiplier, because distribution is the multiplier. The cap is realistically how many products one person can actually maintain at once, not how many will succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. When the single product still wins
&lt;/h2&gt;

&lt;p&gt;The portfolio shape is not universal. Three cases where focusing on a single product is the right call, regardless of the math above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Case 1: high build cost, defensible moat
  If your product needs 600 hours of engineering before the first
  customer can even use it, the attempt cost is too high to run
  a portfolio. Examples: developer infrastructure, a database, a
  language runtime, deep ML, hardware. Pick one. Commit.

Case 2: large total addressable market, slow feedback cycle
  If the buyer has a 6-month evaluation cycle (enterprise SaaS,
  regulated industries, government), the portfolio cannot give you
  enough signal per year. Pick one. Run a long sales cycle.

Case 3: brand-building motion
  If your goal is to become the founder of the thing (the next
  Stripe, the next Linear, the next Figma), the portfolio shape
  fights you. Investors, press, and senior hires want one story.
  Pick one. Tell the story.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are in any of these three cases, run the single product strategy and ignore the portfolio noise. If you are not, the math says you are leaving signal on the table by limiting yourself to one product.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The operating cadence that actually works
&lt;/h2&gt;

&lt;p&gt;Founders who run successful portfolios converge on a similar weekly cadence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mondays:  triage the portfolio. Which products had movement? Which need
          a support reply? What is the metric I am tracking per product
          this week?

Tuesdays-Thursdays: build. Either ship a new product, ship a meaningful
          improvement to a top-3 revenue product, or kill a failing
          product per the contract.

Fridays:  distribution. Post about whatever shipped this week. Engage
          in the channel you own. Reply to comments. No new code.

Saturdays: rest.

Sundays:  one hour of metrics review. Update the portfolio dashboard.
          Decide what next week's primary focus is.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discipline is in the constraint. You do not work on a product unless it appears in Monday's triage. You do not start a new product mid-week. You do not skip Friday distribution because you are "behind on shipping." The cadence is the moat.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The honest take
&lt;/h2&gt;

&lt;p&gt;The portfolio shape is not a moral upgrade over the single product shape. It is a different shape of bet, with its own losing scenarios. The biggest one is brand. A founder with thirty products will never become the founder of one thing. The LinkedIn headline reads "Maker of stuff." The Twitter bio reads as a list. The portfolio founder will not become the next Stripe.&lt;/p&gt;

&lt;p&gt;That tradeoff suits a goal of freedom and revenue. The tradeoff fails a goal of building a category-defining company. Pick the goal honestly. The portfolio gets you out of the day job. The single product, if it works, gets you to the IPO.&lt;/p&gt;

&lt;p&gt;The current decade rewards the portfolio shape more than the previous one did, because the cost of an attempt fell by an order of magnitude. The strategy that matches the new cost is to take more attempts. The kill rule turns those attempts into a long-term system instead of a graveyard.&lt;/p&gt;

&lt;p&gt;Run the calculator. Be honest about your distribution. Pick your shape. Set the kill rule. Then start.&lt;/p&gt;

&lt;p&gt;Question for the comments: how many products do you currently maintain, and what is your actual kill rule (not the one you wish you had)?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The cheapest year of your life is the one where you killed three bad ideas instead of one good one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>indiehackers</category>
      <category>startup</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to read any legacy codebase. The archaeology playbook.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Sun, 17 May 2026 04:25:00 +0000</pubDate>
      <link>https://dev.to/thegdsks/how-to-read-any-legacy-codebase-the-archaeology-playbook-19bh</link>
      <guid>https://dev.to/thegdsks/how-to-read-any-legacy-codebase-the-archaeology-playbook-19bh</guid>
      <description>&lt;h1&gt;
  
  
  How to read any legacy codebase. The archaeology playbook.
&lt;/h1&gt;

&lt;p&gt;Somewhere on a hard drive sits a folder of low resolution scans of Russian typewritten pages from the 1950s. The pages describe PP-BESM, the first high level programming language compiler ever built in the Soviet Union, designed by Andrey Ershov. A developer who goes by xavxav is rebuilding it. Not emulating it. Rebuilding it, line by line, from the scans. The repo is real, the VM runs, the PP-3 phase has an initial pass. You can clone it.&lt;/p&gt;

&lt;p&gt;That project is the extreme version of every "I cannot read this codebase" problem you will ever have at work. Same shape, more dust. The PP-BESM author published a writeup last month that, once you strip the Cold War aesthetic, reads like the cleanest manual on legacy codebase archaeology I have read in years.&lt;/p&gt;

&lt;p&gt;This article is that manual, generalized, with the techniques you can apply this week on whatever inherited PHP, COBOL, Perl, or Java 6 repo is currently your problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Boundaries&lt;/td&gt;
&lt;td&gt;map inputs, outputs, side effects&lt;/td&gt;
&lt;td&gt;you cannot understand the inside until you know the outside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Harness&lt;/td&gt;
&lt;td&gt;build a way to run the code in isolation&lt;/td&gt;
&lt;td&gt;the loop is the whole game&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Bisection&lt;/td&gt;
&lt;td&gt;narrow the search to the load bearing 10 percent&lt;/td&gt;
&lt;td&gt;most code is glue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Naming&lt;/td&gt;
&lt;td&gt;rename systematically as you understand&lt;/td&gt;
&lt;td&gt;you are leaving notes for future you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Types&lt;/td&gt;
&lt;td&gt;add types where there are none, even loose ones&lt;/td&gt;
&lt;td&gt;types are documentation that runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Tests as ground truth&lt;/td&gt;
&lt;td&gt;write tests that lock in observed behavior&lt;/td&gt;
&lt;td&gt;refactoring without tests is fiction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Document negotiations&lt;/td&gt;
&lt;td&gt;comment the why, never the what&lt;/td&gt;
&lt;td&gt;the why is what time erases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The order matters. Skipping ahead is how teams spend six months on "modernization" and end up with a worse version of the same system.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Boundaries before internals
&lt;/h2&gt;

&lt;p&gt;The first move on any unfamiliar codebase is not to read the code. The first move is to draw the boundary.&lt;/p&gt;

&lt;p&gt;For a web service: what HTTP routes exist, what does each one return, what database tables get touched, what external APIs get called, what writes to disk, what fires events. For a CLI: what arguments does it accept, what files does it read, what does it write, what is the exit code matrix. For a library: what is the public API, what does it depend on, what does it monkey-patch.&lt;/p&gt;

&lt;p&gt;You can do this without understanding a single function inside the code. The tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# HTTP routes for a Node service&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"router&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;(get|post|put|delete)|app&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;(get|post)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{js,ts}"&lt;/span&gt; src/

&lt;span class="c"&gt;# Database tables touched&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"FROM|UPDATE|INSERT INTO|DELETE FROM"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{sql,js,ts,py}"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# External API calls&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"axios|fetch&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|http&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;request"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{js,ts}"&lt;/span&gt; src/

&lt;span class="c"&gt;# Files read or written&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"fs&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;(read|write)|open&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{js,ts,py}"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the answers down. This is your map. You cannot understand the internals until you know where the doors are.&lt;/p&gt;

&lt;p&gt;For the PP-BESM project, the boundary was the BESM machine model. You cannot read a 1955 compiler without knowing the instruction set of the machine it targets. xavxav reconstructed that from a separate set of documents before touching the compiler source. Same pattern, smaller stakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build a harness, even a bad one
&lt;/h2&gt;

&lt;p&gt;The highest payoff move on a legacy codebase, by a wide margin, is to get any version of the code running in isolation, with one input and one observable output, before you try to understand any of it.&lt;/p&gt;

&lt;p&gt;For a web service, that means a docker-compose that spins up the app and its database with a single command, with one curl that exercises one route. For a CLI, that means a one-liner that runs the binary with a representative input and pipes the output somewhere you can read it. For a library, that means a five line consumer that imports the library and calls the one function you care about.&lt;/p&gt;

&lt;p&gt;If this is impossible, the rest of the audit will also be impossible. Spend a day building the harness. It is the loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# A minimal harness for a legacy Python script&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; harness
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; harness/run.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
cd "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;/.."
python3 ./scary_script.py --input fixtures/sample.csv &amp;gt; /tmp/out.txt
diff /tmp/out.txt fixtures/expected.txt
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x harness/run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a one-command loop. Every change you make from here on can be tested against &lt;code&gt;harness/run.sh&lt;/code&gt;. The harness is your safety net.&lt;/p&gt;

&lt;p&gt;xavxav's harness for PP-BESM is the BESM virtual machine he built. Every change to the compiler can be tested by running a tiny Soviet-era program inside the VM and watching the result. The VM is more important than any single piece of the compiler source.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bisection beats reading top to bottom
&lt;/h2&gt;

&lt;p&gt;The instinct on a new codebase is to read the entry point and follow the call graph. This is wrong almost every time. Most legacy code is glue. The interesting logic, the part that actually does the work, lives in 10 to 20 percent of the files. The other 80 to 90 percent shuffles data between the interesting parts.&lt;/p&gt;

&lt;p&gt;The fastest way to find the interesting parts is bisection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What touched the database in the last year?&lt;/span&gt;
git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1 year ago"&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format: &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"schema|migration|model"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;

&lt;span class="c"&gt;# Where do the longest files live? long usually means interesting&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# What gets imported the most? heavily imported usually means load bearing&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"^import|^from"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those commands narrows the search. The longest file is often the dumping ground. The most imported module is often the actual brain of the system. The files that show up in every migration are the ones the schema can't live without.&lt;/p&gt;

&lt;p&gt;For PP-BESM the bisection target was PP-3, the last compiler phase. xavxav knew the early phases were better documented in the existing literature. The interesting unknown was the last phase. He focused there first.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Naming as you go
&lt;/h2&gt;

&lt;p&gt;Every time you understand a function, rename it. Every time you understand a variable, rename it. Do this in a branch, and commit often.&lt;/p&gt;

&lt;p&gt;The temptation is to read the whole codebase first and rename later. This is wrong. You will forget what you understood. You will lose hours of context. The rename is the note you are leaving for future you and the next person.&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;// before&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&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;z&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;y&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;z&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// after, you understood this is fetching active user ids over a score threshold&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchActiveUserIdsAboveScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threshold&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;qualifyingIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;users&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;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&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;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qualifyingIds&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;A good rule: if you cannot rename a function meaningfully, you do not understand it yet. Keep reading. Once you can rename it, do it immediately, then commit with a message that captures what you learned.&lt;/p&gt;

&lt;p&gt;xavxav's rename pass on PP-BESM was a translation pass, but the principle is the same. Russian identifiers became English identifiers. Cryptic three letter mnemonics became words. The code became readable because someone took the time to make it readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Types as living documentation
&lt;/h2&gt;

&lt;p&gt;If the codebase is dynamically typed, add types. If the types are wrong, fix them. Even loose types beat no types, because types are the documentation that runs.&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;// before, no types&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculate&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;config&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&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;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taxRate&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="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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// after, types you can refactor against&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LineItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TaxConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LineItem&lt;/span&gt;&lt;span class="p"&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;calculateTotalWithTax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaxConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&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;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taxRate&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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Python, add type hints. For PHP, use PHPStan or Psalm. For old JavaScript, migrate file by file to TypeScript with &lt;code&gt;allowJs: true&lt;/code&gt;. The types do not need to be perfect on day one. They need to exist.&lt;/p&gt;

&lt;p&gt;The reason this matters more than people think: types compile. Comments do not. A wrong comment lives forever. A wrong type breaks the build. Types are the only documentation format that the compiler keeps honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tests as ground truth, even for behavior you do not love
&lt;/h2&gt;

&lt;p&gt;Before you refactor anything, write tests that lock in the observed behavior, including the parts that look like bugs.&lt;/p&gt;

&lt;p&gt;This is the most counterintuitive rule on the list. Junior engineers want to fix the bugs immediately. The right move is to write a test that proves the bug exists first, then keep that test passing while you refactor, then change the test deliberately at the end if the bug should be fixed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pin the current behavior, even if it is wrong
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_calculate_returns_negative_for_empty_orders&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    BUG-LIKE: empty orders currently return -1 instead of 0.
    Some downstream system depends on this. Do not change without
    coordinating with the billing team.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="nc"&gt;TaxConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test does two things. It tells future you that the behavior is intentional, not an accident. It also acts as the alarm if a "small refactor" breaks the contract.&lt;/p&gt;

&lt;p&gt;xavxav's tests for PP-BESM are not unit tests in the modern sense. They are small Soviet-era programs run through the VM with their expected output captured. Same idea, smaller scope. Pin the behavior, refactor against the pin, change the pin deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Comment the negotiations, never the obvious
&lt;/h2&gt;

&lt;p&gt;Your future maintainer can read the code. They cannot read your decision tree. The comments that survive a decade are the ones that capture why a particular choice was made, especially when the choice looks weird.&lt;/p&gt;

&lt;p&gt;Bad comment: &lt;code&gt;// increment counter&lt;/code&gt;. The code already says that.&lt;/p&gt;

&lt;p&gt;Good comment: &lt;code&gt;// We round down because the billing team expects integer cents only. // Historical: float cents caused the May 2023 reconciliation incident.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The good comment is a note from one engineer to another about a constraint that is not visible in the code. The constraint is real. The constraint will outlive the engineer who introduced it. The comment is the only place it lives.&lt;/p&gt;

&lt;p&gt;Run this drill on your legacy codebase: find every place where the code looks slightly odd. A magic number, a hardcoded check, a try/except that swallows a specific exception, a special case for one customer ID. Each one of those is a negotiation that someone made with reality. If the comment is missing, add it once you figure out the negotiation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# bad
&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt;

&lt;span class="c1"&gt;# good
# Set to 47 seconds because their auth gateway has a 50 second hard limit
# and we observed 1-2 second jitter from our load balancer. See incident
# 2024-03-15. Do not raise without coordinating with the partner team.
&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stitching the playbook together
&lt;/h2&gt;

&lt;p&gt;The seven stages are not parallel. They build on each other. The boundary work tells you where to put the harness. The harness lets you bisect. The bisection tells you what to name. The names tell you what to type. The types tell you what to test. The tests give you the safety to comment confidently.&lt;/p&gt;

&lt;p&gt;The same loop runs at every scale. xavxav is running it on a 70 year old compiler with the source on paper. You can run it on a 12 year old Rails app with the source on GitHub. The shape is identical.&lt;/p&gt;

&lt;p&gt;A practical first week, if you are inheriting a legacy codebase tomorrow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Day 1: Boundaries. Draw the map. Do not read internals.
Day 2: Harness. Get any version running with one command.
Day 3: Bisection. Find the 10 percent that does the work.
Day 4: Naming + types. Make the 10 percent readable.
Day 5: Tests. Pin the observed behavior before refactoring.

Week 2 onward: refactor against the pins, comment the negotiations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the end of week one you will know more about the codebase than the engineer who wrote it, because the engineer who wrote it never had the map. They built the system one room at a time. You are reading the architecture in two weeks because the map is part of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest take
&lt;/h2&gt;

&lt;p&gt;Most engineers will tell you they hate legacy codebases. They say this because the only legacy codebases they have seen are the ones nobody bothered to read. A codebase that someone has actually understood, mapped, harnessed, and pinned behavior on, is a perfectly pleasant place to work. The unpleasantness is not in the age of the code, it is in the absence of the archaeology.&lt;/p&gt;

&lt;p&gt;The PP-BESM project will probably never have a million users. It will not show up in your dependency tree. It will not raise a Series A. The project still ranks among the most interesting software writing happening in 2026, because the goal is preservation rather than growth, and because the technique generalizes. The output is not a product. The output is a playbook.&lt;/p&gt;

&lt;p&gt;That playbook works on the codebase that sits in your own repo right now, the one with a &lt;code&gt;legacy/&lt;/code&gt; directory nobody touches. Spend a week on it. The legacy directory will become an asset instead of a liability.&lt;/p&gt;

&lt;p&gt;Question for the comments: what is the oldest piece of code you have ever read seriously, and which of the seven stages did you skip?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every codebase ends up as archaeology eventually. The question is whether anyone bothers to dig.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your bundle is 4000x bigger than Quake. The 9-step audit that fixes it.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Thu, 14 May 2026 04:29:48 +0000</pubDate>
      <link>https://dev.to/thegdsks/your-bundle-is-4000x-bigger-than-quake-the-9-step-audit-that-fixes-it-5cpb</link>
      <guid>https://dev.to/thegdsks/your-bundle-is-4000x-bigger-than-quake-the-9-step-audit-that-fixes-it-5cpb</guid>
      <description>&lt;p&gt;In February 2026 a developer named daivuk shipped a playable Quake-like first person shooter in a 64 kilobyte Windows executable. Multiple levels, four enemy types, textures, music, the whole game. The trick was not magic. He wrote a custom language and a custom virtual machine because the standard toolchain shipped too many features he did not use. Two extra kilobytes of generic runtime would have killed the fourth level.&lt;/p&gt;

&lt;p&gt;That story sat with me for a week, because almost every web app I open is 30 to 60 times the size of QUOD. The page you are reading right now, by the time it finishes loading on Dev.to, weighs more than four hundred copies of QUOD running at once. The marketing page for the framework your app is built on is heavier than QUOD by three orders of magnitude. We have collectively forgotten what bytes cost.&lt;/p&gt;

&lt;p&gt;This article is the audit playbook I use when a Next.js or Vite project crosses my desk and the Lighthouse score reads orange. Nine steps, in the exact order, with the commands, the expected output, and the typical wins. Everything you need to cut your bundle by 50 to 90 percent in a single afternoon. No "rewrite in Rust" theater. Just deletions.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you run&lt;/th&gt;
&lt;th&gt;Typical win&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Baseline&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npx next build&lt;/code&gt; then read the output table&lt;/td&gt;
&lt;td&gt;knowing where you stand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Visualise&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@next/bundle-analyzer&lt;/code&gt; or &lt;code&gt;rollup-plugin-visualizer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Kill date libraries&lt;/td&gt;
&lt;td&gt;swap &lt;code&gt;moment&lt;/code&gt; for &lt;code&gt;date-fns&lt;/code&gt; or native &lt;code&gt;Intl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;50 to 90 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Kill icon sets&lt;/td&gt;
&lt;td&gt;one import per icon, never the full pack&lt;/td&gt;
&lt;td&gt;20 to 200 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Kill lodash&lt;/td&gt;
&lt;td&gt;swap &lt;code&gt;lodash&lt;/code&gt; for &lt;code&gt;lodash-es&lt;/code&gt; or native&lt;/td&gt;
&lt;td&gt;60 to 80 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Audit polyfills&lt;/td&gt;
&lt;td&gt;drop IE 11 support; target ES2022&lt;/td&gt;
&lt;td&gt;30 to 100 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Code-split routes&lt;/td&gt;
&lt;td&gt;dynamic imports for non-critical pages&lt;/td&gt;
&lt;td&gt;100 KB to 1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Replace images&lt;/td&gt;
&lt;td&gt;AVIF or modern WebP, properly sized&lt;/td&gt;
&lt;td&gt;200 KB to 2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Re-baseline&lt;/td&gt;
&lt;td&gt;run step 1 again, write the number down&lt;/td&gt;
&lt;td&gt;confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numbers in the table come from documented case studies on web.dev, the HTTP Archive 2025 annual report, and the Vercel Next.js docs. Your mileage will vary. The order will not.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Baseline
&lt;/h2&gt;

&lt;p&gt;You cannot improve what you have not measured. Before you touch anything, get an honest number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Next.js&lt;/span&gt;
npx next build
&lt;span class="c"&gt;# read the "First Load JS" table at the bottom&lt;/span&gt;

&lt;span class="c"&gt;# Vite&lt;/span&gt;
npx vite build
&lt;span class="c"&gt;# read the dist/ output sizes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number you want is "First Load JS shared by all," and then your largest individual route. Write both down. This number will be your accountability for the rest of the audit. If it does not go down by at least 30 percent by step 9, you skipped something or you have a genuinely small project, which is fine, you are done.&lt;/p&gt;

&lt;p&gt;The HTTP Archive's 2025 annual web almanac reports a median JavaScript transfer size of 612 KB on desktop and 555 KB on mobile. If your number is meaningfully bigger than that, you have low hanging fruit. If it is meaningfully smaller, you are already ahead of most of the industry.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Visualise the bundle
&lt;/h2&gt;

&lt;p&gt;A list of files is not a map. You need the map.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Next.js&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @next/bundle-analyzer
&lt;span class="c"&gt;# in next.config.js wrap your config with the analyzer&lt;/span&gt;
&lt;span class="nv"&gt;ANALYZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Vite&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; rollup-plugin-visualizer
&lt;span class="c"&gt;# add it to vite.config.ts&lt;/span&gt;
npx vite build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analyzer opens a treemap in your browser. The treemap is the entire audit's source of truth. Every fat block is a question. Every question is one of the next seven steps.&lt;/p&gt;

&lt;p&gt;Spend ten minutes here. Hover the rectangles. Find the ones that are unfamiliar. The ones you cannot explain are the ones that have the most byte fat.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Kill the date library
&lt;/h2&gt;

&lt;p&gt;The single most common bundle bloat in the entire JavaScript ecosystem. Moment.js is 67 KB minified before gzip. day.js is 7 KB. date-fns with tree shaking can drop to 12 KB. Native &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; is zero.&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;// before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;moment&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;moment&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;formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY-MM-DD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// after, native, zero bytes added&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-CA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// or with date-fns, tree shakes cleanly&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;format&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;date-fns&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;formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yyyy-MM-dd&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 a global grep for &lt;code&gt;moment&lt;/code&gt; and &lt;code&gt;dayjs&lt;/code&gt; in your codebase. If you find moment, you have a 50 to 90 KB win sitting on the floor. The migration is mechanical and well documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Kill the icon set import
&lt;/h2&gt;

&lt;p&gt;The second most common bundle bloat, especially in dashboards built on Material UI, Chakra, or any "we have icons" library. The trap is the default import.&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;// before, ships the entire icon set&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;Search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Menu&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;@mui/icons-material&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// after, ships only the three icons&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Search&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;@mui/icons-material/Search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&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;@mui/icons-material/Person&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Menu&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;@mui/icons-material/Menu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default barrel import in many icon packs is the entire 2 MB of SVG. The per-icon import path ships only what you reference. Material UI's documentation explicitly warns about this. Many teams ignore it. Check yours.&lt;/p&gt;

&lt;p&gt;For Lucide, Heroicons, theSVG and Phosphor, tree-shaking generally works correctly if your bundler is set up right. Verify it in the analyzer. If you see the full icon library in your treemap, the tree shake did not happen and you need to fix the import path.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Kill the utility library
&lt;/h2&gt;

&lt;p&gt;Lodash is 70 KB. Most apps use seven functions from it. The fix is either &lt;code&gt;lodash-es&lt;/code&gt; with tree shaking, or replacing the seven functions with native equivalents.&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;// before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&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;lodash&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;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&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;unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// after, native&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&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;unique&lt;/span&gt; &lt;span class="o"&gt;=&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="c1"&gt;// or, tree shaken&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;groupBy&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;lodash-es/groupBy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;uniq&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;lodash-es/uniq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Object.groupBy&lt;/code&gt; shipped in 2024 and is widely available. &lt;code&gt;Map.groupBy&lt;/code&gt; is also there. The Set constructor handles uniqueness in one line. Underscore is even worse than lodash for the same reason. Check your dependency tree, find them, replace them, save bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Audit the polyfill load
&lt;/h2&gt;

&lt;p&gt;If your project supports browsers older than the last two years of Chrome, Safari, and Firefox, you are shipping polyfills you do not need. The &lt;code&gt;.browserslistrc&lt;/code&gt; or &lt;code&gt;browserslist&lt;/code&gt; field in package.json governs this.&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;before&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"browserslist"&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;gt; 0.5%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"last 2 versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Firefox ESR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not dead"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;after,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;modern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;targets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"browserslist"&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;"last 2 chrome versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"last 2 firefox versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="s2"&gt;"last 2 safari versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"last 2 edge versions"&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;The wins here vary by project. A React app that explicitly targets IE 11 ships about 50 KB more than the same app targeting last-two-versions. Vue and Svelte have similar ratios. Check the analyzer for &lt;code&gt;core-js&lt;/code&gt;, &lt;code&gt;regenerator-runtime&lt;/code&gt;, &lt;code&gt;@babel/runtime&lt;/code&gt;. Each of those is a polyfill bundle, and each shrinks meaningfully when you raise the target.&lt;/p&gt;

&lt;p&gt;The honest tradeoff: if you serve enterprise customers stuck on Internet Explorer, you cannot do this. Almost everyone else can.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Code split by route
&lt;/h2&gt;

&lt;p&gt;The biggest single lever. Most apps load every component on every page because the bundler does not know which routes need what. The fix is dynamic imports for non-critical paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before, eager import&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HeavyDashboard&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;./HeavyDashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// after, lazy&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;lazy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Suspense&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;react&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;HeavyDashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./HeavyDashboard&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;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HeavyDashboard&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;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;In Next.js the App Router does most of this automatically per route. The wins come from splitting heavy components inside a route. A chart library, a markdown editor, a video player, a payment SDK. Each of those is a candidate.&lt;/p&gt;

&lt;p&gt;Run the analyzer again after this step. The shared bundle should drop by 100 KB to a megabyte, depending on what you split. The page-specific bundles will be larger, but only loaded when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Replace your images
&lt;/h2&gt;

&lt;p&gt;Almost forgot the part where pictures of food account for 70 percent of the bytes on the average e-commerce page.&lt;/p&gt;

&lt;p&gt;The 2026 image stack is straightforward. Serve AVIF with a WebP fallback and a JPEG fallback. Size them to the actual display dimensions, not the original camera resolution. Use the native &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element or a framework wrapper like &lt;code&gt;next/image&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero.avif"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The width and height attributes prevent layout shift and give the browser an early hint. The loading=lazy attribute defers off-screen images. The AVIF source typically shaves 30 to 50 percent off the file size compared to JPEG at the same quality.&lt;/p&gt;

&lt;p&gt;A typical e-commerce site that does the full image audit drops its page weight by a megabyte or two. That single change moves Lighthouse scores more than the previous six steps combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Re-baseline and write the number down
&lt;/h2&gt;

&lt;p&gt;Run step 1 again. Write the new number next to the old one. Compare.&lt;/p&gt;

&lt;p&gt;If you ran all eight changes on a typical Next.js app with one heavy dashboard, an icon library, lodash, and unoptimized images, you should see the First Load JS drop from a starting point of 400 to 600 KB down to 100 to 200 KB. The Lighthouse performance score should jump 20 to 40 points. The Time to Interactive should fall by a full second on a throttled mid-range Android device.&lt;/p&gt;

&lt;p&gt;If you did not get those wins, one of two things happened. Either your app is already lean, in which case congratulations, or you skipped a step. Run the analyzer again and find the rectangle that is still too big.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework you can actually keep
&lt;/h2&gt;

&lt;p&gt;The nine steps above are a one-time audit. The hard part is keeping the wins after the audit ends. Three rules I run on every project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Rule 1: A bundle budget &lt;span class="k"&gt;in &lt;/span&gt;CI.
  Bundle size has to be a number &lt;span class="k"&gt;in &lt;/span&gt;a green or red box on every PR.
  npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; bundlewatch
  Add it to your &lt;span class="nb"&gt;test &lt;/span&gt;script. Set a max. Fail the build on regression.

Rule 2: A dependency review on every PR that touches package.json.
  Use the @sentry/bundle-analyzer or @next/bundle-analyzer &lt;span class="k"&gt;in &lt;/span&gt;CI.
  Post the diff as a comment. The team will see it. The team will care.

Rule 3: A monthly &lt;span class="s2"&gt;"what got fat"&lt;/span&gt; report.
  Once a month, run the analyzer and look at the biggest rectangles.
  One of them will surprise you. Fix it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without these three rules the wins drift back inside six months. With them, the bundle stays at the size you decided it should be at the audit, indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest take
&lt;/h2&gt;

&lt;p&gt;You are not going to ship your next SaaS in 64 KB. Nobody is asking you to. But the lesson from QUOD is not about the absolute number, it is about the constraint mindset. The standard toolchain ships every feature you do not use. Every dependency is a vote against your users on a slow connection. Every imported icon set is a tax on the laptop battery of the person reading your page on a flight.&lt;/p&gt;

&lt;p&gt;The good news is that the audit pays back in hours, not weeks. The first time I ran this playbook on a real codebase, I cut a 540 KB First Load JS down to 168 KB in one afternoon. The before and after Lighthouse score difference would have taken six months of "performance work" if I had done it gradually. Doing it all in one focused sweep is dramatically faster.&lt;/p&gt;

&lt;p&gt;The next time you reach for a 4 MB library to format a date, think about QUOD. Then think about whether your users would rather download your full app, or four hundred copies of QUOD running at the same time, with guns in them.&lt;/p&gt;

&lt;p&gt;Question for the comments: what is the biggest single byte win you ever shipped, and what tool did you replace?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every byte in your bundle is a tiny vote against your users on a slow connection.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I asked Cursor to rename a function. It sent 8,400 tokens. I checked.</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Wed, 13 May 2026 03:58:11 +0000</pubDate>
      <link>https://dev.to/thegdsks/i-asked-cursor-to-rename-a-function-it-sent-8400-tokens-i-checked-434h</link>
      <guid>https://dev.to/thegdsks/i-asked-cursor-to-rename-a-function-it-sent-8400-tokens-i-checked-434h</guid>
      <description>&lt;p&gt;&lt;em&gt;The afternoon I learned what my AI subscription was actually doing, and the 200 lines that took my next bill down 41 percent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I had been using Cursor for six months when I noticed the discrepancy. I was renaming a function. A short one. Three lines of body. One call site. The kind of refactor that takes, at most, six seconds of human attention.&lt;/p&gt;

&lt;p&gt;I had two windows open. The Cursor chat panel where I had typed "rename getUser to fetchUser". And the Anthropic console in another tab, because I had been debugging a different project earlier and forgot to close it.&lt;/p&gt;

&lt;p&gt;The Anthropic console refreshed while the Cursor request was in flight. I watched the token counter tick up live. The number it landed on for that single rename request was 8,400 input tokens. The actual prompt I had typed was eleven words.&lt;/p&gt;

&lt;p&gt;I sat there for a moment. Then I opened a fresh terminal and made the same call directly through the Anthropic API with my own minimal prompt. Same model. Same intent. Same outcome.&lt;/p&gt;

&lt;p&gt;The direct call used 1,900 input tokens. Cursor had sent 6,500 extra tokens of context to perform the same rename.&lt;/p&gt;

&lt;p&gt;That observation was the start of the spreadsheet that ate the next four hours of my evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was in those 6,500 tokens
&lt;/h2&gt;

&lt;p&gt;I do not have inside knowledge of Cursor's internals. I have my own experiments and the public documentation, neither of which gives a complete picture. Here is what I have: when I ran the same prompt 50 times across different parts of my codebase, the input token count varied between roughly 4,000 and 14,000 with a median around 8,000. The variation correlated loosely with how many open buffers I had and how recently I had viewed files in the same directory.&lt;/p&gt;

&lt;p&gt;The reasonable inference is that Cursor was sending me a system prompt, plus indexing context derived from my recent activity, plus tool definitions for the agent framework, plus the actual prompt I had typed. The first three are the routing layer doing its job. The fourth was the only one I could see in the chat panel.&lt;/p&gt;

&lt;p&gt;Some of that context helps. When I ask for a refactor that touches several files, Cursor knowing about those files is the entire point. When I ask to rename a function whose call sites fit in three lines of context, sending 6,500 tokens of unrelated buffer state is the routing layer playing it safe on my behalf in a way that benefits Cursor more than it benefits me.&lt;/p&gt;

&lt;p&gt;Cursor charges a fixed seat fee. Anthropic charges Cursor by the token. The math runs the wrong direction for me as a heavy user, because the marginal token cost ends up in my own direct API calls (which Cursor's seat does not cover) plus the fixed seat itself. Conservative context is cheap to ship and expensive to consume. The incentive lands on the wrong side of the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill that made me pay attention
&lt;/h2&gt;

&lt;p&gt;My March bill arrived on April 2. The Anthropic line item had grown 50 percent month over month for three months running. Cursor at $20 was flat. Copilot at $10 was flat. The variable line was the API I was hitting from my own CLI for things Cursor was not the right tool for.&lt;/p&gt;

&lt;p&gt;The growth was not from doing more work. I checked. My weekly logged hours were stable. The growth was from an increasing fraction of those hours involving AI calls that I was making more casually because the AI was getting more useful.&lt;/p&gt;

&lt;p&gt;The trend was straight. If I extrapolated, by August I was going to be paying more in direct Anthropic API spend than in Cursor seats, and my Cursor seat would still be running the same conservative context overhead on every chat panel turn. The bill was going to keep growing in two places at once.&lt;/p&gt;

&lt;p&gt;I cancelled Cursor that weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 200 lines
&lt;/h2&gt;

&lt;p&gt;The thing I built to replace the routing piece of Cursor was small enough to embarrass me for not having built it months earlier. A regex based intent classifier with five rules. Trivial prompts route to Haiku. Code prompts route to Sonnet. Planning prompts route to Opus. Embedding-style classification prompts route to a cheap OpenAI model. Default to Sonnet if nothing matches.&lt;/p&gt;

&lt;p&gt;That is the entire routing logic. Two hundred lines of TypeScript including imports, error handling, a pricing table, and a cost calculator that logs every call. The full file fits on one screen if you have a tall display.&lt;/p&gt;

&lt;p&gt;I tested it on a hundred prompts I had logged from the previous week. The breakdown shifted hard. Sonnet went from 70 percent of calls to 25 percent. Haiku went from zero to 60 percent. Opus stayed at 5 percent. The estimated cost reduction was 47 percent on the test set.&lt;/p&gt;

&lt;p&gt;I did not believe the number. I assumed I had a bug. I instrumented the router to log the actual model picked per prompt and the cost in real time, and I ran my normal workflow for two weeks. The actual reduction came in at 41 percent on the May 2 bill, with 30 percent more total calls because the cheaper per call cost made me reach for AI more often.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not understand before
&lt;/h2&gt;

&lt;p&gt;The routing layer is the most valuable part of the AI tool stack right now, and the wrappers want to own that part most of all.&lt;/p&gt;

&lt;p&gt;Every coding tool I have looked at in the last 90 days has shipped a model dropdown. Cursor added one. GitHub Copilot added one. Windsurf added one. The story they tell is customer choice. The story underneath, I think, is that they have all noticed the same thing I noticed in April. The user can route their own calls. The user is starting to. If the wrappers do not own the routing layer, they own a chat panel and an autocomplete and not much else.&lt;/p&gt;

&lt;p&gt;The chat panel is real value. The autocomplete is real value. They are not $20 a month of value for a heavy user who would prefer to route directly. They are maybe $5 to $10 a month of value, sized to the actual work they save.&lt;/p&gt;

&lt;p&gt;I think we are two quarters away from a wave of users doing this exercise. The wrappers know that. The pricing pages are starting to reflect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell my March self
&lt;/h2&gt;

&lt;p&gt;Three things, in the order they matter.&lt;/p&gt;

&lt;p&gt;The first: open the Anthropic dashboard. Look at the input token count on three of your normal Cursor turns. If the number runs more than 3x your direct call baseline, the routing layer is not earning its cost on your usage pattern. That does not mean cancel. That means notice.&lt;/p&gt;

&lt;p&gt;The second: log every AI call you make for one week. Cost per call, model picked, prompt length, output length. The log takes twenty lines of code per provider. The data will surprise you. No honest way exists to optimize a bill you cannot see.&lt;/p&gt;

&lt;p&gt;The third: write the router. Two hundred lines. The first version does not have to be smart. Five regex rules for intent capture 70 percent of the savings. Iterate on the rules later.&lt;/p&gt;

&lt;p&gt;The reason I would tell my March self these things: I would have done the same exercise three months earlier and saved roughly $300 in subscription overlap. The cost of doing the exercise: one Saturday afternoon. The cost of not doing it: whatever your bill grows to in the next quarter, which for most people building with AI right now lands at a number bigger than they want to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;The router does not give me a multi file editing agent. It does not index my codebase. It does not know about my open buffers. It does not autocomplete inline. None of that is its job.&lt;/p&gt;

&lt;p&gt;I kept Copilot for the inline ghost text in VS Code, because that is a different product solving a different problem and the $10 is not the line that hurts. For the multi file agent work I would have used Cursor for, I now use Claude Code from the terminal, which I pay for separately through my Max plan. The total stack is cheaper than Cursor plus my old direct API spend.&lt;/p&gt;

&lt;p&gt;If your usage pattern is different, your math will be different. If you live in the chat panel and rarely go outside it, Cursor is probably still a fair trade. If your AI work spans chat, agent loops, embedding pipelines, and one off CLI calls, the routing layer is the one piece worth owning yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The closing
&lt;/h2&gt;

&lt;p&gt;The bill came in on April 2. The new bill came in on May 2. The difference between them was 41 percent and 200 lines of code and one weekend afternoon I was going to spend half asleep in front of a movie.&lt;/p&gt;

&lt;p&gt;The lesson should have been obvious. The wrappers have an incentive to send more tokens than necessary. The user has an incentive to send fewer. The routing layer is where the two incentives meet. Whoever owns the routing layer wins.&lt;/p&gt;

&lt;p&gt;The routing layer can be 200 lines of yours.&lt;/p&gt;

&lt;p&gt;What is the line on your AI bill that grew the fastest last month? Drop it in a reply. I read everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **GDS K S&lt;/em&gt;* (&lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt;), building &lt;a href="https://glincker.com" rel="noopener noreferrer"&gt;Glincker&lt;/a&gt;.*&lt;br&gt;
&lt;em&gt;If this was useful, follow me on &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;X / @thegdsks&lt;/a&gt;. I write about the parts of the AI stack vendors keep off the pricing page.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cursor</category>
      <category>claude</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AWS Lambda Is Dead. The $0.20 Was Never the Price</title>
      <dc:creator>GDS K S</dc:creator>
      <pubDate>Tue, 12 May 2026 04:24:50 +0000</pubDate>
      <link>https://dev.to/thegdsks/aws-lambda-is-dead-the-020-was-never-the-price-2k4j</link>
      <guid>https://dev.to/thegdsks/aws-lambda-is-dead-the-020-was-never-the-price-2k4j</guid>
      <description>&lt;p&gt;Last quarter we migrated 47 Lambda functions off AWS. The monthly bill dropped from $8,362 to $1,790. Lambda invocations were 22% of that bill. The rest was the part AWS never put on the pricing page, never put in the AWS Lambda 101 docs, and never came up when our solutions architect ran our forecast workshop in 2024.&lt;/p&gt;

&lt;p&gt;We are committing to one position in this piece and we are not flipping at the end. Lambda is dead for the API, webhook, auth, and edge workload that most teams actually deploy. Every comparison article we read while researching the move closed with "but evaluate your needs carefully." That sentence is what kept us on Lambda for an extra year. We are not writing that sentence today.&lt;/p&gt;

&lt;p&gt;If you are skimming, the punchline is at the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why Lambda loses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INIT billing (Aug 2025)&lt;/td&gt;
&lt;td&gt;Cold start init time now bills like duration&lt;/td&gt;
&lt;td&gt;The price floor moved up, quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The orchestration bundle&lt;/td&gt;
&lt;td&gt;API Gateway, CloudWatch, NAT, egress&lt;/td&gt;
&lt;td&gt;Lambda is 20 to 40% of your serverless bill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The crossover point&lt;/td&gt;
&lt;td&gt;Where Fargate or Workers wins&lt;/td&gt;
&lt;td&gt;Moved from 20M to about 2M invocations a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V8 isolates&lt;/td&gt;
&lt;td&gt;Cold start of 2 to 5ms on Workers&lt;/td&gt;
&lt;td&gt;No warm-up tax, no provisioned concurrency to buy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. The $0.20 Per Million Is a Loss Leader
&lt;/h2&gt;

&lt;p&gt;Lambda lists at $0.20 per million requests and $0.0000166667 per GB-second. People build their forecasts on this number. Then the bill arrives and the Lambda line item is a quarter of the total.&lt;/p&gt;

&lt;p&gt;A breakdown from a real account, redacted, one month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda invocations + duration   $1,847   (22%)
API Gateway                     $1,612   (19%)
CloudWatch Logs                 $1,398   (17%)
NAT Gateway hours               $1,287   (15%)
Data egress                     $1,094   (13%)
X-Ray, KMS, Secrets, parameter store   $1,124   (14%)
                              ─────────
Total                          $8,362
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffn1nga3oqwgdstvelty0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffn1nga3oqwgdstvelty0.png" alt="Cost Breakdown - GLINR" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read that table again. Lambda is the fifth column. The wrapper. CloudWatch Logs is bigger. NAT Gateway is bigger. Almost everything is bigger.&lt;/p&gt;

&lt;p&gt;Lambda by itself was fine. Lambda inside the AWS bundle was a loss leader for the bundle. CloudWatch Logs at the default ingestion rate eats functions that log a single audit line per call. NAT Gateway hours rack up if your function needs to reach a private RDS or any non-VPC endpoint, which is, you know, basically every real function. Data egress at $0.09 per gigabyte compounds with every response payload.&lt;/p&gt;

&lt;p&gt;We have a name for this pattern on our internal docs. The Token Tab. The headline price advertises the wrapper. The real money is in the orchestration around it. AWS did not invent this pattern. AWS just runs the most disciplined version of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. INIT Billing Changed the Floor (And Nobody Noticed)
&lt;/h2&gt;

&lt;p&gt;In August 2025, AWS quietly changed how Lambda billed cold start initialization. Before the change, INIT time was free for managed runtimes. Now it bills as duration.&lt;/p&gt;

&lt;p&gt;For a JVM function with 800ms of init, every cold invocation costs an extra 80% on top of the actual work. Boot up Spring Boot? Pay for the boot. Cold start your fat Python ML container? Pay for the import storm.&lt;/p&gt;

&lt;p&gt;The change shipped in a release note. Not a keynote. Not a tweet from a Principal Engineer. A release note.&lt;/p&gt;

&lt;p&gt;We did not find out about it until April 2026 when we audited a function that was supposed to cost $40 and was billing $110.&lt;/p&gt;

&lt;p&gt;Reports floating around early 2026 (&lt;a href="https://medium.com/infradecodedops/aws-lambda-is-your-worst-performing-cost-saver-the-2026-cold-start-data-will-shock-you-0822418e57c8" rel="noopener noreferrer"&gt;source: InfraDecodedOps cold-start teardown&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;23% of customer-facing Lambda invocations hit a cold start&lt;/li&gt;
&lt;li&gt;p99 cold latency of 1.8 seconds on those endpoints&lt;/li&gt;
&lt;li&gt;AWS counters that fewer than 1% of invocations across Lambda are cold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both numbers are true. The 1% headline reflects hot, internal, async functions running at scale. The 23% reflects what your users actually feel on your auth endpoint at 6:42am UTC when traffic is sparse.&lt;/p&gt;

&lt;p&gt;SnapStart helps for Java, Python, .NET. SnapStart does not help for the part of your bill that is API Gateway and NAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Crossover Moved (And It Moved a Lot)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ib7lr30517fbhi4onob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ib7lr30517fbhi4onob.png" alt="Crossover GRAPH" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Folk wisdom in 2022 was that Lambda was cheaper until you hit roughly 20 million invocations a month. After that, Fargate. After that, real instances.&lt;/p&gt;

&lt;p&gt;With INIT billing layered onto the bundle pricing, &lt;a href="https://dev.to/alanwest/aws-lambdas-hidden-costs-when-to-migrate-to-containers-and-how-2h1n"&gt;the crossover is closer to 2 million&lt;/a&gt; for typical API workloads. That is an order of magnitude shift in three years. Nobody updated the blog posts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monthly cost
   ▲
   │                    ╱── Lambda + bundle
   │              ╱────╱
   │        ╱────╱
   │  ╱────╱─────────── Fargate (1 task, ALB)
   │═════════════════════ Workers + KV/D1
   │
   └──────────────────────►
    0    1M   2M    5M   10M  invocations/mo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers stays flat because Workers bills CPU time, not wall clock. If your function waits 200ms on a database response but only burns 15ms of CPU, you pay for 15ms. Lambda bills for the full 200ms.&lt;/p&gt;

&lt;p&gt;That is not a small detail. That is the whole game. The typical API spends 80% of its time waiting on a database, a downstream service, or an LLM. Lambda charges you for the wait. Workers does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Cope Chart (Optimizations That Do Not Save You)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqjexak1f75irjun1tjge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqjexak1f75irjun1tjge.png" alt="Cope Chart" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Things we tried in 2024 and 2025 to "fix" our Lambda bill:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tactic&lt;/th&gt;
&lt;th&gt;Effect on Lambda line&lt;/th&gt;
&lt;th&gt;Effect on total bill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trim function memory&lt;/td&gt;
&lt;td&gt;-8% on Lambda&lt;/td&gt;
&lt;td&gt;-1.7% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch Python to Node&lt;/td&gt;
&lt;td&gt;-12% on Lambda&lt;/td&gt;
&lt;td&gt;-2.6% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARM64 (Graviton2)&lt;/td&gt;
&lt;td&gt;-15% on Lambda&lt;/td&gt;
&lt;td&gt;-3.3% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add provisioned concurrency&lt;/td&gt;
&lt;td&gt;+30% on Lambda&lt;/td&gt;
&lt;td&gt;+6.6% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SnapStart on JVM functions&lt;/td&gt;
&lt;td&gt;-22% on Lambda&lt;/td&gt;
&lt;td&gt;-4.8% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit and prune CloudWatch retention&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;-9% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add VPC endpoints to kill NAT&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;-14% on total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You see the pattern. The Lambda optimizations move the Lambda line a little. The bundle optimizations move the bill a lot. We spent a year picking up dimes on the Lambda line while the bundle was charging us in 50s.&lt;/p&gt;

&lt;p&gt;The audit-and-kill-bundle tactics are what actually save money on AWS. Nobody writes blog posts about them because they are not sexy. There is no AWS re:Invent talk about deleting your unused log groups. There should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Workers vs Lambda, Side by Side
&lt;/h2&gt;

&lt;p&gt;A real example. A signed URL generator for S3, ported to R2 on Workers.&lt;/p&gt;

&lt;p&gt;Lambda version:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetObjectCommand&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="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;getSignedUrl&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="s2"&gt;@aws-sdk/s3-request-presigner&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;s3&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;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;key&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;pathParameters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;key&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&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="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing key&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;cmd&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;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&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;url&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;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&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="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&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;Cold start with the SDK loads at 400 to 700ms. Warm at 30 to 80ms. Bills wall clock. Behind API Gateway. Logs go to CloudWatch by default. Outbound to S3 may hit NAT depending on VPC config.&lt;/p&gt;

&lt;p&gt;Workers version with R2:&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;export&lt;/span&gt; &lt;span class="k"&gt;default&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;req&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="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&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;key&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;URL&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="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="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;key&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing key&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&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;obj&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;obj&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not found&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache-control&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="s2"&gt;public, max-age=300&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="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;Cold start at 2 to 5ms (&lt;a href="https://blog.rebalai.com/en/2026/03/09/cloudflare-workers-vs-aws-lambda-which-edge-runtim/" rel="noopener noreferrer"&gt;Cloudflare's own dashboards&lt;/a&gt; and a six-month production comparison back this up). R2 egress to the public internet is zero. No log retention to forget about. No NAT in the path. The pricing page does not need a footnote.&lt;/p&gt;

&lt;p&gt;Same function. Different platform. The bill says everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  5b. The Platform Comparison
&lt;/h2&gt;

&lt;p&gt;Here is the table that should be at the top of every serverless conversation in 2026. Pricing is approximate and cold starts come from production benchmarks reported by independent teams. The links go to canonical comparison or pricing pages for each platform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Cold start&lt;/th&gt;
&lt;th&gt;Billing model&lt;/th&gt;
&lt;th&gt;Free egress&lt;/th&gt;
&lt;th&gt;Sweet spot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.vantage.sh/blog/cloudflare-workers-vs-aws-lambda-cost" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;200-700ms (cold), bills INIT since Aug 2025&lt;/td&gt;
&lt;td&gt;Wall clock + bundle&lt;/td&gt;
&lt;td&gt;No ($0.09/GB)&lt;/td&gt;
&lt;td&gt;AWS-native event glue, GPU jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://leaper.dev/blog/cloudflare-workers-vs-lambda-2026" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2-5ms (V8 isolate)&lt;/td&gt;
&lt;td&gt;CPU time only&lt;/td&gt;
&lt;td&gt;Yes (R2 zero)&lt;/td&gt;
&lt;td&gt;HTTP APIs, edge, webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://blog.rebalai.com/en/2026/03/09/cloudflare-workers-vs-aws-lambda-which-edge-runtim/" rel="noopener noreferrer"&gt;Google Cloud Run&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;50-200ms&lt;/td&gt;
&lt;td&gt;Wall clock&lt;/td&gt;
&lt;td&gt;Yes within region&lt;/td&gt;
&lt;td&gt;Container portability, ML inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://leanopstech.com/blog/aws-lambda-pricing-2026/" rel="noopener noreferrer"&gt;Azure Functions Premium&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pre-warmed (0)&lt;/td&gt;
&lt;td&gt;Flat + per-exec&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Enterprise, no-cold-start needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/alanwest/aws-lambdas-hidden-costs-when-to-migrate-to-containers-and-how-2h1n"&gt;Fly.io Machines&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;250ms-2s (cold)&lt;/td&gt;
&lt;td&gt;Per-second machine&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Stateful regional apps, full VM control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://leaper.dev/blog/cloudflare-workers-vs-lambda-2026" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Container boot&lt;/td&gt;
&lt;td&gt;Usage-based&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Solo and small team backend hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://leaper.dev/blog/cloudflare-workers-vs-lambda-2026" rel="noopener noreferrer"&gt;Baselime reported&lt;/a&gt; an 80% cloud-cost drop after migrating from AWS to Cloudflare. &lt;a href="https://www.sitepoint.com/case-study-cloud-to-local-ai-pwa/" rel="noopener noreferrer"&gt;Sitepoint published a case study&lt;/a&gt; where a team moved their Lambda proxy entirely into the browser and cut their bill from $2,400 to $140 a month. Different workloads, same direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Honest Counter (And Why It Does Not Save Lambda)
&lt;/h2&gt;

&lt;p&gt;Three places Lambda still earns its keep. We are not pretending otherwise.&lt;/p&gt;

&lt;p&gt;GPU and long-running compute. Workers has a 30-second CPU cap and no GPU. If you are doing inference, video transcoding, or anything past a 30-second budget, Lambda or SageMaker or a real instance is the answer. We kept zero of these. We do not run any.&lt;/p&gt;

&lt;p&gt;Heavy AWS-native event glue. If your workload is S3 to DynamoDB to SQS to Step Functions and you never leave AWS, the orchestration bundle is the platform, and Lambda is the right glue. We kept four functions for this. They run for cents.&lt;/p&gt;

&lt;p&gt;Sparse async cron jobs. A nightly batch at 3am that runs once a day for 200ms? Lambda is essentially free at that scale. Workers Cron Triggers are also fine. Either works.&lt;/p&gt;

&lt;p&gt;Notice what is absent from that list. The typical API. The webhook receiver. The edge function. The auth gateway. The fan-out worker that most teams actually deploy. That whole pile is the dead zone for Lambda in 2026.&lt;/p&gt;

&lt;p&gt;If your team's Lambda fleet is mostly the typical API workload, you are in the dead zone whether you have noticed yet or not. The bill will tell you eventually. Usually after a quarterly investor update where someone asks why infrastructure spend grew faster than revenue.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The Migration Playbook
&lt;/h2&gt;

&lt;p&gt;If you want to move the workloads that no longer make sense on Lambda:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull a 30-day Cost Explorer report. Tag by Lambda function name. Find the top 10 by total cost.&lt;/li&gt;
&lt;li&gt;For each, count outbound calls per invocation, average duration, average response size, cold start frequency, and whether it crosses a NAT.&lt;/li&gt;
&lt;li&gt;Candidates that move first are HTTP-fronted, low-CPU, high-invocation, network-bound. Those are the ones bleeding wall-clock dollars.&lt;/li&gt;
&lt;li&gt;Port to Workers or Cloud Run. Keep the AWS-native event glue on Lambda. Do not try a big-bang migration. Move one function, watch it for two days, move the next.&lt;/li&gt;
&lt;li&gt;Watch the bill for two cycles. Decommission API Gateway routes and CloudWatch log groups as functions go quiet. The teardown is where 30% of the savings actually live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We did this over six weeks. The hardest part was not the rewrites. The hardest part was untangling which CloudWatch log groups still mattered and which ones quietly cost $40 a month to keep indexed. The second hardest part was convincing one of our seniors that the AWS SDK he had written wrappers around for two years was the part we were throwing out.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What We Are Not Coming Back For
&lt;/h2&gt;

&lt;p&gt;Lambda was the right shape in 2015. The shape of cloud has moved. V8 isolates that start in 2ms. Container platforms with sub-second cold boots. Edge runtimes that bill CPU instead of wall clock. Those are the new default for the workload Lambda used to own.&lt;/p&gt;

&lt;p&gt;We will not migrate back. Not because Workers is perfect, but because the bundle math does not reverse. If AWS dropped Lambda pricing to $0.10 per million tomorrow, our bill would drop by 11%. The other 89% would still be the orchestration tax.&lt;/p&gt;

&lt;p&gt;The $0.20 per million was a beautiful marketing line. That was never the price. The price was always the bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If your team is building an API in 2026 and Lambda is the first thing on the architecture diagram, ask why. Ask it out loud. The honest answer is almost always organizational momentum, not technical fit. Someone built this pattern at their last job. The first hire wired it up because the AWS reference architecture said to. Nobody questioned it because nobody had time to audit Cost Explorer.&lt;/p&gt;

&lt;p&gt;We had that conversation. Six weeks later our bill was 79% smaller and our p99 was 22 times faster.&lt;/p&gt;

&lt;p&gt;Lambda is dead for the workload most teams actually run. The pricing page never told you why. The bill always will.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GDS K S&lt;/strong&gt; · &lt;a href="https://thegdsks.com" rel="noopener noreferrer"&gt;thegdsks.com&lt;/a&gt; · follow on X &lt;a href="https://x.com/thegdsks" rel="noopener noreferrer"&gt;@thegdsks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lambda is dead for the workloads most teams actually run. The bundle around it was always the real product.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://medium.com/infradecodedops/aws-lambda-is-your-worst-performing-cost-saver-the-2026-cold-start-data-will-shock-you-0822418e57c8" rel="noopener noreferrer"&gt;Sandesh, &lt;em&gt;AWS Lambda Is Your Worst-Performing Cost-Saver: The 2026 Cold Start Data&lt;/em&gt;&lt;/a&gt;. Source for 23% cold start figure and 1.8s p99.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://leanopstech.com/blog/aws-lambda-pricing-2026/" rel="noopener noreferrer"&gt;LeanOps, &lt;em&gt;AWS Lambda Pricing 2026: Costs, Fees and Hidden Traps&lt;/em&gt;&lt;/a&gt;. Source for 20-40% base Lambda share of total spend.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/alanwest/aws-lambdas-hidden-costs-when-to-migrate-to-containers-and-how-2h1n"&gt;Alan West, &lt;em&gt;AWS Lambda's Hidden Costs: When to Migrate to Containers&lt;/em&gt;&lt;/a&gt;. Source for the crossover point math.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.vantage.sh/blog/cloudflare-workers-vs-aws-lambda-cost" rel="noopener noreferrer"&gt;Vantage, &lt;em&gt;Cloudflare Workers vs AWS Lambda Cost&lt;/em&gt;&lt;/a&gt;. Source for CPU-time vs wall-clock pricing breakdown.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://leaper.dev/blog/cloudflare-workers-vs-lambda-2026" rel="noopener noreferrer"&gt;Leaper, &lt;em&gt;Cloudflare Workers vs AWS Lambda 2026&lt;/em&gt;&lt;/a&gt;. Source for 50-80% cost reduction at scale and Baselime case.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.rebalai.com/en/2026/03/09/cloudflare-workers-vs-aws-lambda-which-edge-runtim/" rel="noopener noreferrer"&gt;Rebal AI, &lt;em&gt;Cloudflare Workers vs AWS Lambda: Six Months of Production Reality&lt;/em&gt;&lt;/a&gt;. Source for six-month production comparison.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.sitepoint.com/case-study-cloud-to-local-ai-pwa/" rel="noopener noreferrer"&gt;SitePoint, &lt;em&gt;Case Study: Cloud to Local-First AI Migration&lt;/em&gt;&lt;/a&gt;. Source for the $2,400 to $140 PWA migration.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
