<?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: Ali Ahmed</title>
    <description>The latest articles on DEV Community by Ali Ahmed (@ali_ahmed_44ee99051e327ce).</description>
    <link>https://dev.to/ali_ahmed_44ee99051e327ce</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4229287%2F1dd33913-386e-4f06-9bc3-7d95cab91313.jpg</url>
      <title>DEV Community: Ali Ahmed</title>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ali_ahmed_44ee99051e327ce"/>
    <language>en</language>
    <item>
      <title>How to stop an LLM from inventing numbers: evidence containment and numeral tracing</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:37:12 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/how-to-stop-an-llm-from-inventing-numbers-evidence-containment-and-numeral-tracing-28l9</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/how-to-stop-an-llm-from-inventing-numbers-evidence-containment-and-numeral-tracing-28l9</guid>
      <description>&lt;p&gt;You can't prompt an LLM out of inventing statistics, but you can make an invented number &lt;strong&gt;impossible to publish&lt;/strong&gt;. In my &lt;a href="https://github.com/QuantumBreakz/Linkedin-Automation" rel="noopener noreferrer"&gt;Research-to-LinkedIn Automation&lt;/a&gt; project, which turns a researcher's new papers into LinkedIn posts, the model extracts and drafts, while &lt;strong&gt;deterministic code decides what counts as true&lt;/strong&gt;. Every "stated" claim needs a verbatim quote that is actually in the paper, any number without one is thrown away, and every numeral in the final draft must trace back to the source. If code says no, the draft fails, whatever the model thinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are invented numbers the worst hallucination?
&lt;/h2&gt;

&lt;p&gt;Posting about someone's research under their name means one wrong number is a public misquote of their own work. A vague sentence is embarrassing. A fabricated "37% reduction" is a correction request. So the design treats &lt;strong&gt;invented statistics as the highest-severity failure&lt;/strong&gt;, and spends its strictest checks there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: How do you make extraction honest?
&lt;/h2&gt;

&lt;p&gt;Extraction is schema-driven (Zod plus the provider's structured-output mode), so there's no free-text parsing. Each claim carries a &lt;strong&gt;provenance&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;STATED&lt;/code&gt;&lt;/strong&gt;: the paper says it. &lt;code&gt;evidence&lt;/code&gt; must be a verbatim quote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INFERRED&lt;/code&gt;&lt;/strong&gt;: a reasonable conclusion that isn't stated directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ABSENT&lt;/code&gt;&lt;/strong&gt;: the paper doesn't say. The prompt tells the model this is &lt;strong&gt;correct and preferred over guessing&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two structural choices do most of the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask only for what the source can support.&lt;/strong&gt; If only the abstract is available, the schema doesn't even have fields for methodology, dataset or limitations. The model can't make up a methodology because there's nowhere to put one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence is computed, not self-reported.&lt;/strong&gt; It's the share of claims that survive verification, not a number the model gives itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: What is evidence containment?
&lt;/h2&gt;

&lt;p&gt;After the model answers, code checks every &lt;code&gt;STATED&lt;/code&gt; claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;containsEvidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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="nx"&gt;evidence&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="nx"&gt;boolean&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;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evidence&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evidence&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;ul&gt;
&lt;li&gt;A &lt;code&gt;STATED&lt;/code&gt; claim whose quote &lt;strong&gt;isn't in the paper&lt;/strong&gt; is downgraded to &lt;code&gt;INFERRED&lt;/code&gt;, and its fake evidence is cleared.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;important number&lt;/strong&gt; whose quote isn't in the paper is &lt;strong&gt;discarded entirely&lt;/strong&gt;. Numbers don't get a downgrade path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's plain substring matching after normalising case and whitespace. That's crude, and that's the point: a quote either appears in the paper or it doesn't, and no model gets a vote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: How do you verify the finished draft?
&lt;/h2&gt;

&lt;p&gt;The drafting model writes from the extraction, not the paper, so errors can creep back in. The draft then goes through &lt;strong&gt;two independent checks in parallel&lt;/strong&gt;, and both must pass:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A. An LLM fact-checker&lt;/strong&gt; labels each claim &lt;code&gt;SUPPORTED&lt;/code&gt;, &lt;code&gt;HEDGED_OK&lt;/code&gt;, &lt;code&gt;UNSUPPORTED&lt;/code&gt; or &lt;code&gt;CONTRADICTED&lt;/code&gt;, and flags overstatement and anything that reads like medical advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B. Deterministic checks&lt;/strong&gt;, which &lt;strong&gt;override&lt;/strong&gt; the LLM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numeral tracing.&lt;/strong&gt; Every numeral in the draft (including forms like &lt;code&gt;12.5%&lt;/code&gt; or &lt;code&gt;3×&lt;/code&gt;) must appear in the verified important numbers or literally in the source text. One unmatched number fails the draft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Causal-language guard.&lt;/strong&gt; If a sentence uses "causes", "proves", "cures", "prevents" or similar, and the backing finding is phrased as correlation ("associated", "suggests", "may"), the draft fails for overstatement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule from the design doc: &lt;strong&gt;if a check can be decided in code, code decides it.&lt;/strong&gt; An LLM judge is good at nuance and bad at reliably noticing that 0.37 became 37%.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't this catch?
&lt;/h2&gt;

&lt;p&gt;Being honest about the gaps matters as much as the checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A real number in the wrong place.&lt;/strong&gt; Tracing confirms a number exists in the source, not that the draft attaches it to the right finding. The LLM pass is what catches that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paraphrase drift.&lt;/strong&gt; A claim can be reworded until it no longer means what the quote supports. Containment checks the evidence, not the paraphrase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Substring matching is literal.&lt;/strong&gt; A quote broken by a hyphenation artefact from PDF extraction can fail even though it's real. That's the safe direction to fail in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The general pattern
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Let the model say &lt;strong&gt;"I don't know"&lt;/strong&gt; (&lt;code&gt;ABSENT&lt;/code&gt;), and reward it.&lt;/li&gt;
&lt;li&gt;Make every claim carry &lt;strong&gt;verifiable evidence&lt;/strong&gt;, and verify it in code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete&lt;/strong&gt;, don't downgrade, anything high-stakes that fails.&lt;/li&gt;
&lt;li&gt;Use an LLM judge for nuance, with &lt;strong&gt;deterministic checks holding the veto&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Related: &lt;a href="https://aliahmed.si/blog/agent-tool-error-messages/" rel="noopener noreferrer"&gt;writing tool errors that AI agents understand&lt;/a&gt;, and &lt;a href="https://aliahmed.si/blog/oauth-mcp-server-crm/" rel="noopener noreferrer"&gt;building an OAuth 2.1 MCP server&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/stop-llm-inventing-numbers/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aiengineering</category>
      <category>llms</category>
      <category>hallucinations</category>
      <category>evaluation</category>
    </item>
    <item>
      <title>Labels vs annotations: how one YAML indent made Prometheus scrape Pyroscope twice</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:36:36 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/labels-vs-annotations-how-one-yaml-indent-made-prometheus-scrape-pyroscope-twice-5a7d</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/labels-vs-annotations-how-one-yaml-indent-made-prometheus-scrape-pyroscope-twice-5a7d</guid>
      <description>&lt;p&gt;A Prometheus &lt;code&gt;ServiceMonitor&lt;/code&gt; selects Services by &lt;strong&gt;labels&lt;/strong&gt;. It never looks at &lt;strong&gt;annotations&lt;/strong&gt;. In Grafana Pyroscope's Helm chart, a refactor nested &lt;code&gt;prometheus.io/service-monitor: "false"&lt;/code&gt; under &lt;code&gt;annotations&lt;/code&gt; instead of &lt;code&gt;labels&lt;/code&gt; on the headless Service, so the exclusion silently stopped working. Prometheus scraped every pod twice, as &lt;code&gt;job="pyroscope"&lt;/code&gt; and &lt;code&gt;job="pyroscope-headless"&lt;/code&gt;. My fix, &lt;a href="https://github.com/grafana/pyroscope/pull/5377" rel="noopener noreferrer"&gt;grafana/pyroscope#5377&lt;/a&gt; (in review), moves the key back under &lt;code&gt;metadata.labels&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the difference between labels and annotations in Kubernetes?
&lt;/h2&gt;

&lt;p&gt;Both are key-value metadata, but they do different jobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Labels&lt;/th&gt;
&lt;th&gt;Annotations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Identify and &lt;strong&gt;select&lt;/strong&gt; objects&lt;/td&gt;
&lt;td&gt;Attach non-identifying information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used by selectors (Services, ServiceMonitors, kubectl &lt;code&gt;-l&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical content&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app&lt;/code&gt;, &lt;code&gt;component&lt;/code&gt;, &lt;code&gt;tier&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;build info, tool configuration, long text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some tools read &lt;em&gt;annotations&lt;/em&gt; for configuration, like the old &lt;code&gt;prometheus.io/scrape&lt;/code&gt; convention for annotation-based scraping. That's exactly why the two get mixed up. But the Prometheus Operator's &lt;code&gt;ServiceMonitor&lt;/code&gt; uses a &lt;strong&gt;label selector&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did the chart exclude the headless Service?
&lt;/h2&gt;

&lt;p&gt;Pyroscope runs a normal Service and a headless one pointing at the same pods. Scraping both would duplicate every metric, so the chart's &lt;code&gt;ServiceMonitor&lt;/code&gt; excludes Services marked with a label:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```yaml title="ServiceMonitor selector"&lt;br&gt;
selector:&lt;br&gt;
  matchExpressions:&lt;br&gt;
    - key: prometheus.io/service-monitor&lt;br&gt;
      operator: NotIn&lt;br&gt;
      values:&lt;br&gt;
        - "false"&lt;/p&gt;

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


The headless Service carries `prometheus.io/service-monitor: "false"`, so it's left out. That only works if the key is a **label**.

## What broke?

A change that added support for custom `headlessAnnotations` (#4451) changed the YAML nesting in `services.yaml`. The `prometheus.io/service-monitor: "false"` line ended up under `metadata.annotations`.

Nothing failed. `helm template` still rendered valid YAML, the Service still worked, and Prometheus still scraped. It just scraped the same pod IPs twice under two job names (issue #5215): duplicate series, double the storage for those metrics, and dashboards that sum by job quietly counting double.

## How did I fix it?

* I moved the `prometheus.io/service-monitor: "false"` block so it renders under `metadata.labels` for the headless Service.
* I kept the `headlessAnnotations` feature from #4451 working.
* I bumped the chart version (2.1.2 → 2.1.3) and regenerated the rendered golden fixtures with `make helm/check`.

You can verify it with:



```bash title="Check the rendered headless Service"
helm template . -s templates/services.yaml --set serviceMonitor.enabled=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and check that the key now sits under &lt;code&gt;metadata.labels&lt;/code&gt; on the headless Service.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you catch this class of bug?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test the selection, not just the rendering.&lt;/strong&gt; A golden-file test that asserts "the headless Service has label X" would have failed when the key moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for duplicate series.&lt;/strong&gt; The same &lt;code&gt;instance&lt;/code&gt; showing up under two &lt;code&gt;job&lt;/code&gt; values is a classic sign of double scraping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review YAML nesting changes like logic changes.&lt;/strong&gt; In Helm templates, indentation &lt;em&gt;is&lt;/em&gt; the logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More upstream fixes, from LiteLLM to Node.js, are on my &lt;a href="https://aliahmed.si/projects/" rel="noopener noreferrer"&gt;projects page&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/servicemonitor-labels-vs-annotations/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>prometheus</category>
      <category>helm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Streaming LLM output through a Next.js proxy: why you only get stream_end</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:31:47 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/streaming-llm-output-through-a-nextjs-proxy-why-you-only-get-streamend-23j3</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/streaming-llm-output-through-a-nextjs-proxy-why-you-only-get-streamend-23j3</guid>
      <description>&lt;p&gt;When you proxy an LLM backend's &lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt; through a Next.js route handler, the usual failure is that nothing arrives until the end, and then everything arrives at once, or the client only ever sees the final &lt;code&gt;stream_end&lt;/code&gt;. Something between the backend and the browser is &lt;strong&gt;buffering&lt;/strong&gt;. The fix is a handful of specific things: the Node.js runtime, &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt;, the right headers (&lt;code&gt;Cache-Control: no-cache, no-transform&lt;/code&gt; and &lt;code&gt;X-Accel-Buffering: no&lt;/code&gt;), passing bytes through as they arrive, and a client that parses SSE frames properly. I learned these building &lt;strong&gt;Turbo Alan Refiner&lt;/strong&gt;, a multi-pass LLM text-refinement app (&lt;a href="https://github.com/QuantumBreakz/TurboAlanRefinerFrontend" rel="noopener noreferrer"&gt;frontend source&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why proxy SSE through Next.js at all?
&lt;/h2&gt;

&lt;p&gt;Routing every backend call through &lt;code&gt;/api/*&lt;/code&gt; route handlers avoids CORS, keeps tokens server-side, and lets you normalise the request. The cost is that your route is now a hop in a streaming pipeline, and every hop is a chance to buffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the route handler need?
&lt;/h2&gt;



&lt;p&gt;```ts title="Route segment config"&lt;br&gt;
export const runtime = "nodejs";&lt;br&gt;
export const dynamic = "force-dynamic";&lt;br&gt;
export const maxDuration = 300;&lt;/p&gt;

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


* `runtime = "nodejs"` gives full streaming support without edge-runtime limits.
* `dynamic = "force-dynamic"` stops Next.js caching or pre-rendering the route.
* `maxDuration = 300` lets long refinements outlive the default function timeout.

Then return a `ReadableStream` that forwards the upstream body as it arrives, with these headers:



```ts title="Streaming response headers"
return new Response(stream, {
  headers: {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache, no-transform",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no",
  },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;X-Accel-Buffering: no&lt;/code&gt; tells nginx-style proxies not to buffer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;no-transform&lt;/code&gt; matters more than it looks. A proxy that gzips the response has to collect bytes before it can compress them, which is exactly the buffering you're trying to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should the proxy forward events?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read with &lt;code&gt;getReader()&lt;/code&gt; and decode with a streaming &lt;code&gt;TextDecoder&lt;/code&gt;.&lt;/strong&gt; Never &lt;code&gt;await upstream.text()&lt;/code&gt;, which waits for the whole response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward events, don't re-wrap them.&lt;/strong&gt; If the upstream already sends &lt;code&gt;data: {...}&lt;/code&gt; and your proxy prefixes &lt;code&gt;data:&lt;/code&gt; again, the client receives &lt;code&gt;data: data: {...}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send errors as events.&lt;/strong&gt; If the upstream fails mid-stream, emit a final &lt;code&gt;data: {"type":"error",...}&lt;/code&gt; frame so the UI can show it, instead of the stream just stopping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass the client's abort on.&lt;/strong&gt; Use an &lt;code&gt;AbortController&lt;/code&gt; so closing the tab cancels the upstream request, rather than leaving an LLM job running for nobody.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why does the client only see stream_end?
&lt;/h2&gt;

&lt;p&gt;Usually one of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Something buffered the stream&lt;/strong&gt; (missing headers, the wrong runtime, a proxy in front), so all the events arrived together and the UI only rendered the last one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The client parsed chunks, not lines.&lt;/strong&gt; A network chunk can end in the middle of an event. Keep the incomplete tail in a buffer:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;```ts title="Client: keep the incomplete last line"&lt;br&gt;
   buffer += decoder.decode(chunk, { stream: true });&lt;br&gt;
   const lines = buffer.split("\n");&lt;br&gt;
   buffer = lines.pop() ?? "";&lt;/p&gt;

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


3. **The client stopped too early.** Treat only true terminal events (`complete`, `error`) as the end. Keep processing everything already buffered before you close.

Be lenient about input, too: strip a doubled `data: data: ` prefix, and accept raw JSON lines as well as SSE-formatted ones.

## How do you debug it?

Open DevTools → Network → the streaming request → **EventStream**. If events show up one by one there but the UI updates all at once, the bug is in your client. If they all show up together, something upstream is buffering: check the headers, the runtime, and any proxy or CDN in front.

Related: [what a serverless cold start really is](https://aliahmed.si/blog/serverless-cold-starts-explained/) (long-lived streams and function timeouts run into the same platform limits).

---

Written by Ali Ahmed · [Original article](https://aliahmed.si/blog/nextjs-sse-proxy-streaming/)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>nextjs</category>
      <category>streaming</category>
      <category>llms</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Real-time KYC card verification over MQTT: a pub-sub pipeline explained</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:31:11 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/real-time-kyc-card-verification-over-mqtt-a-pub-sub-pipeline-explained-nm7</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/real-time-kyc-card-verification-over-mqtt-a-pub-sub-pipeline-explained-nm7</guid>
      <description>&lt;p&gt;You can build a real-time &lt;strong&gt;KYC (Know Your Customer) card verification&lt;/strong&gt; pipeline from small independent services that talk through an &lt;strong&gt;MQTT broker&lt;/strong&gt;: one publishes card data, one verifies it and publishes a result &lt;em&gt;with reasons&lt;/em&gt;, and one stores and visualises the results. Each service only knows topic names, not the other services. That's the design of &lt;a href="https://github.com/QuantumBreakz/MQTT-K-Project" rel="noopener noreferrer"&gt;this project&lt;/a&gt;, which I built with two teammates on Mosquitto, Python 3.12, SQLite and a Flask dashboard. My part was the card client, the service that generates and publishes cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use MQTT for verification?
&lt;/h2&gt;

&lt;p&gt;MQTT is a lightweight publish/subscribe protocol. Producers publish messages to &lt;strong&gt;topics&lt;/strong&gt;, consumers subscribe to them, and a &lt;strong&gt;broker&lt;/strong&gt; in the middle routes messages. For a verification pipeline that buys you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling.&lt;/strong&gt; The verifier doesn't know who sends cards, and the analyst doesn't know who verifies them. Any of them can be replaced, scaled or restarted alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real time.&lt;/strong&gt; Results flow as soon as each card is checked, with no batch job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery guarantees&lt;/strong&gt; chosen per message, through QoS levels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How does the pipeline fit together?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card client&lt;/strong&gt; publishes ID card records to &lt;code&gt;kyc/card_data&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier&lt;/strong&gt; subscribes to &lt;code&gt;kyc/card_data&lt;/code&gt;, validates each field, and publishes to &lt;code&gt;kyc/result&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyst&lt;/strong&gt; subscribes to &lt;code&gt;kyc/result&lt;/code&gt;, stores results in SQLite, builds charts and exports CSV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; (Flask) reads the analyst's data and shows live totals, approvals and the rejection rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do you test a verifier properly?
&lt;/h2&gt;

&lt;p&gt;With bad input, on purpose. The card client makes &lt;strong&gt;about 30% of the cards invalid&lt;/strong&gt;, with malformed ID numbers, one-word names and expired cards, alongside valid ones. Each card has an ID, a name, an expiry date, a region and a card type. A pipeline tested only on clean data proves very little.&lt;/p&gt;

&lt;p&gt;The client also retries its broker connection (up to 5 attempts) instead of dying on the first failure, because services start in any order.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the verifier check?
&lt;/h2&gt;

&lt;p&gt;Every field, and it &lt;strong&gt;records why&lt;/strong&gt; a card failed rather than just "rejected":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ID format:&lt;/strong&gt; &lt;code&gt;NNNN-NNNN-NNNN&lt;/code&gt; or &lt;code&gt;NNNNNN-NNNN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry:&lt;/strong&gt; not in the past&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; at least two words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region:&lt;/strong&gt; one of a known list (US, EU, ASIA, MEA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card type:&lt;/strong&gt; one of the accepted types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A card passes only if every check passes. The published result carries the status &lt;strong&gt;plus the list of reasons&lt;/strong&gt;, so a rejected card says "Invalid ID format, Card expired", not just "no". In real KYC, a rejection the operator can't explain is a support ticket waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does QoS 1 mean for the design?
&lt;/h2&gt;

&lt;p&gt;The services publish and subscribe at &lt;strong&gt;QoS 1, "at least once"&lt;/strong&gt;. The broker keeps sending a message until it's acknowledged, so nothing is lost, but a message &lt;strong&gt;can arrive twice&lt;/strong&gt;, for example after a reconnect.&lt;/p&gt;

&lt;p&gt;That has a consequence I'd design for in a production version: every consumer must be &lt;strong&gt;idempotent&lt;/strong&gt;. A verifier that appends every message it receives would count a duplicated card twice. Storing results keyed by card ID (upserting, not appending) makes duplicates harmless. QoS 2 ("exactly once") avoids duplicates at the protocol level, but costs an extra round trip per message.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does this compare to production identity verification?
&lt;/h2&gt;

&lt;p&gt;This was a course project, deliberately simple: rule-based checks on structured fields. Production KYC, which I worked on at ShuftiPro, starts one step earlier, from a &lt;strong&gt;photo of the document&lt;/strong&gt;. Computer vision finds and classifies the document, OCR reads it in the right script, face matching and liveness checks confirm the person, and only then do field checks like these run. The pipeline shape is the same, though: independent stages, explicit reasons, and test data that is deliberately bad.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://aliahmed.si/blog/uber-real-time-location-system/" rel="noopener noreferrer"&gt;the Uber system design breakdown&lt;/a&gt; uses push messaging for the same reason, and &lt;a href="https://aliahmed.si/blog/voice-ai-webhook-security/" rel="noopener noreferrer"&gt;the voice agent webhook post&lt;/a&gt; covers idempotency for repeated events.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/mqtt-kyc-verification-pipeline/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kyc</category>
      <category>mqtt</category>
      <category>distributedsystems</category>
      <category>python</category>
    </item>
    <item>
      <title>How I got my first pull request merged into LiteLLM</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:26:22 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/how-i-got-my-first-pull-request-merged-into-litellm-58p1</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/how-i-got-my-first-pull-request-merged-into-litellm-58p1</guid>
      <description>&lt;p&gt;My first contribution to &lt;strong&gt;LiteLLM&lt;/strong&gt;, the open-source LLM gateway, was &lt;a href="https://github.com/BerriAI/litellm/pull/38591" rel="noopener noreferrer"&gt;PR #38591&lt;/a&gt;. It fixed a bug where content an agent had retrieved was compressed again and lost, which made agents loop. It was merged. If you want to make your first open-source contribution, the short version is: pick a project you already use, reproduce one specific bug, fix only that bug, and let the project's CI teach you its standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was the bug?
&lt;/h2&gt;

&lt;p&gt;LiteLLM can compress long context before sending it to a model. Large content is swapped for a compact reference, and the model can call a retrieval tool with that reference to get the full content back when it needs it.&lt;/p&gt;

&lt;p&gt;The bug hit clients that &lt;strong&gt;run their own tool loop&lt;/strong&gt;. When the agent asked for the full content, the retrieved text went back through compression and was &lt;strong&gt;replaced by the same reference again&lt;/strong&gt;. The agent never saw the content it had asked for, so it either asked again (a loop) or answered from an empty skeleton.&lt;/p&gt;

&lt;p&gt;The fix was to &lt;strong&gt;stop re-compressing content that had just been retrieved&lt;/strong&gt; in those client-side tool loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you find something to work on?
&lt;/h2&gt;

&lt;p&gt;I wasn't hunting for a famous repository. I was using LiteLLM, had some free time, and saw something I could pick up. That's the best starting point, because you already understand the problem as a user.&lt;/p&gt;

&lt;p&gt;What worked for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start from your own friction.&lt;/strong&gt; A bug you've hit is a bug you can reproduce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the issue tracker for "good first issue" or a recent regression,&lt;/strong&gt; and check nobody has already claimed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce it locally first.&lt;/strong&gt; A failing test or a minimal script is worth more than a long description.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did the CI teach me?
&lt;/h2&gt;

&lt;p&gt;LiteLLM is a big, fast-moving repository, and its CI/CD is thorough: linting, type checks, a large test suite and more. Going through those checks was one of the most useful parts. I picked up ideas I've since applied to my own projects, including at &lt;a href="https://aliahmed.si/experience/" rel="noopener noreferrer"&gt;SmartLogics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A big project's CI is a free code review of your habits. Let it fail, read why, and fix it properly rather than working around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you get a PR merged?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep it small.&lt;/strong&gt; One bug, one fix, one test. Reviewers merge what they can understand quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain the failure, not just the change.&lt;/strong&gt; Steps to reproduce, what was expected, what happened.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add the test that would have caught it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respond quickly to review comments.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What came next?
&lt;/h2&gt;

&lt;p&gt;That first PR made the next ones easier. I've since contributed to &lt;strong&gt;Node.js&lt;/strong&gt; (counting buffered HTTP output in bytes, not string length, so backpressure works), &lt;strong&gt;Symfony&lt;/strong&gt;, &lt;strong&gt;Grafana Pyroscope&lt;/strong&gt;, &lt;strong&gt;LibrePods&lt;/strong&gt;, and &lt;strong&gt;NanoClaw&lt;/strong&gt;, where I wrote a fix for an &lt;a href="https://aliahmed.si/blog/localhost-is-not-authentication/" rel="noopener noreferrer"&gt;unauthenticated loopback webhook&lt;/a&gt;. The full list is on my &lt;a href="https://aliahmed.si/projects/" rel="noopener noreferrer"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The best part wasn't the code. It was seeing that a small, careful fix to a project used by thousands of people is completely within reach. Here's to many more.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/first-litellm-contribution/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>aiengineering</category>
      <category>llms</category>
    </item>
    <item>
      <title>From 10 µm to 3 nm: how chips got so small (and why 3 nm isn't 3 nm)</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:25:46 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/from-10-um-to-3-nm-how-chips-got-so-small-and-why-3-nm-isnt-3-nm-1fnn</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/from-10-um-to-3-nm-how-chips-got-so-small-and-why-3-nm-isnt-3-nm-1fnn</guid>
      <description>&lt;p&gt;In 1971 Intel's first microprocessor, the &lt;strong&gt;4004&lt;/strong&gt;, was built on a &lt;strong&gt;10 micrometre&lt;/strong&gt; process and held about &lt;strong&gt;2,300 transistors&lt;/strong&gt;. Today's leading chips are marketed as &lt;strong&gt;3 nm&lt;/strong&gt; and hold &lt;strong&gt;tens of billions&lt;/strong&gt; of transistors. The largest packages pass 100 billion. Shrinking transistors lets you fit more onto the same silicon, which means more parallel work, more cache and more specialised hardware. The catch: "3 nm" is now a product name, not a measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  How big is 10 micrometres?
&lt;/h2&gt;

&lt;p&gt;A human hair is roughly 70–100 µm thick, so the 4004's features were already a fraction of a hair. 10 µm is &lt;strong&gt;10,000 nm&lt;/strong&gt;. On the node names alone, going from 10,000 to 3 is a shrink of more than &lt;strong&gt;3,000 times&lt;/strong&gt;, while transistor counts grew by a factor of &lt;strong&gt;millions&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do smaller transistors mean faster chips?
&lt;/h2&gt;

&lt;p&gt;A transistor is an electronic switch. Every calculation, image and AI model comes down to billions of switches turning on and off. Smaller transistors give you three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More of them&lt;/strong&gt; in the same area: more cores, bigger caches, dedicated AI and media engines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less energy per switch&lt;/strong&gt;, so more work per watt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shorter distances&lt;/strong&gt; for signals to travel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is a 3 nm chip actually 3 nanometres?
&lt;/h2&gt;

&lt;p&gt;No. Until the late 1990s the node name roughly matched a real physical dimension (the transistor gate length). Since then it has become a &lt;strong&gt;generation label&lt;/strong&gt;. No feature on a "3 nm" chip is 3 nm wide; real gate pitches are several times larger. The industry now judges progress by &lt;strong&gt;transistor density&lt;/strong&gt; and by performance and power at each new node, not by the name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is shrinking so hard now?
&lt;/h2&gt;

&lt;p&gt;At these sizes, engineers are fighting physics more than machinery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leakage:&lt;/strong&gt; electrons tunnel through barriers that should block them, wasting power when a switch is meant to be off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heat:&lt;/strong&gt; more switches in less space means more heat per square millimetre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variation:&lt;/strong&gt; an error of a few atoms can ruin a transistor, and one bad transistor can ruin a chip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How did the industry keep going?
&lt;/h2&gt;

&lt;p&gt;By reinventing the transistor itself, more than once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Planar transistors:&lt;/strong&gt; a flat channel with the gate on top. This worked for decades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FinFETs&lt;/strong&gt; (from about 2011): the channel stands up as a fin, and the gate wraps it on three sides for better control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate-all-around (GAA) / nanosheets&lt;/strong&gt; (2 nm-class nodes): the gate wraps the channel completely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Patterning moved to &lt;strong&gt;extreme ultraviolet (EUV) lithography&lt;/strong&gt;, which uses 13.5 nm light to print features that older light sources couldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;A modern chip isn't a scaled-down 1971 chip. Decades of materials science, quantum physics, chemistry, optics and computer engineering separate them. The story of computing is partly the story of humans learning to control matter with absurd precision, and we're still finding ways to go smaller.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://aliahmed.si/blog/gpu-architecture-explained/" rel="noopener noreferrer"&gt;what's inside a GPU&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/chip-process-nodes-explained/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>semiconductors</category>
    </item>
    <item>
      <title>One phone number, two users: normalising WhatsApp sender IDs across Baileys and the Cloud API</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:11:36 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/one-phone-number-two-users-normalising-whatsapp-sender-ids-across-baileys-and-the-cloud-api-3l3m</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/one-phone-number-two-users-normalising-whatsapp-sender-ids-across-baileys-and-the-cloud-api-3l3m</guid>
      <description>&lt;p&gt;If a system can receive messages from the same person through two integrations, &lt;strong&gt;both must produce the same user ID&lt;/strong&gt;, or that person silently becomes two users with different permissions. In &lt;strong&gt;NanoClaw&lt;/strong&gt;, the Baileys WhatsApp integration identified a sender as &lt;code&gt;15551234567@s.whatsapp.net&lt;/code&gt; (a JID), while the WhatsApp Cloud API path used the bare &lt;code&gt;wa_id&lt;/code&gt;, &lt;code&gt;15551234567&lt;/code&gt;. Roles and group memberships granted to one identity never applied to the other. My fix (&lt;a href="https://github.com/nanocoai/nanoclaw/pull/3070" rel="noopener noreferrer"&gt;#3070&lt;/a&gt;, in review) normalises the handle at one point and migrates existing data, merging the duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did one number become two users?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Sender handle&lt;/th&gt;
&lt;th&gt;Stored user ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baileys (native WhatsApp Web protocol)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;15551234567@s.whatsapp.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whatsapp:15551234567@s.whatsapp.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp Cloud API (via a chat SDK bridge)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;15551234567&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whatsapp:15551234567&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both paths use the channel type &lt;code&gt;whatsapp&lt;/code&gt;, so the two IDs became two rows in &lt;code&gt;users&lt;/code&gt;. Anyone moving a number from Baileys to the Cloud API, or running both at once, found that the approvals, roles and agent-group memberships they'd set up "didn't work". In fact they'd been granted to a different user.&lt;/p&gt;

&lt;p&gt;The cause was one line: the code built the user ID directly from whatever raw handle the adapter emitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you normalise identifiers?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick one canonical form.&lt;/strong&gt; Here that's bare digits, which the Cloud API already emits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalise at one point, as early as possible.&lt;/strong&gt; A &lt;code&gt;normalizeWhatsAppHandle(channelType, handle)&lt;/code&gt; helper strips the &lt;code&gt;@s.whatsapp.net&lt;/code&gt; domain and the optional &lt;code&gt;:device&lt;/code&gt; suffix, and it's applied exactly where the user ID is built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope it narrowly.&lt;/strong&gt; It only touches WhatsApp &lt;strong&gt;user&lt;/strong&gt; JIDs. Group JIDs (&lt;code&gt;@g.us&lt;/code&gt;) and every other channel pass through unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't normalise identifiers that mean something else.&lt;/strong&gt; The messaging-group routing ID is left as the raw JID on purpose, because routing depends on it. "Who sent this" and "where to reply" are different identities.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How do you fix data that's already split?
&lt;/h2&gt;

&lt;p&gt;Normalising new messages isn't enough: existing installations already have both rows. A one-time migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rewrites&lt;/strong&gt; every user ID stored in JID form to the canonical form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascades&lt;/strong&gt; the rename through every table that references users: roles, agent-group members, DMs, and the optional approval and dropped-message tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merges&lt;/strong&gt; when a canonical row already exists (because the person also messaged through the Cloud API). Roles and memberships are &lt;strong&gt;unioned&lt;/strong&gt; into the canonical row and the JID row is deleted, so nobody loses access in the merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs in a transaction&lt;/strong&gt;, with the migration runner checking foreign-key integrity before it commits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;Any system with more than one way in (two messaging providers, email plus OAuth, a web app plus an API) needs a &lt;strong&gt;canonical identity&lt;/strong&gt; decided at the boundary. Otherwise permission bugs show up as "it works for some people", which is the hardest kind to debug. For agent platforms the stakes are higher: identity decides who may approve what an agent does.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://aliahmed.si/blog/localhost-is-not-authentication/" rel="noopener noreferrer"&gt;localhost is not authentication&lt;/a&gt;, another NanoClaw fix.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/whatsapp-sender-identity-normalization/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>aiagents</category>
      <category>database</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>What actually gets deployed to production? (Not your code)</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:10:55 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/what-actually-gets-deployed-to-production-not-your-code-5d2j</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/what-actually-gets-deployed-to-production-not-your-code-5d2j</guid>
      <description>&lt;p&gt;What gets deployed to production is &lt;strong&gt;not your source code but a build artifact&lt;/strong&gt;: an immutable snapshot your pipeline produced from it, such as a Docker image, a compiled Next.js output, a Java JAR or a serverless package. Production servers never pull your repository or compile TypeScript on demand. They run the artifact. That one idea explains rollbacks, environment changes and most of CI/CD.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 4 of the Modern Deployments series. Previous: &lt;a href="https://aliahmed.si/blog/serverless-cold-starts-explained/" rel="noopener noreferrer"&gt;serverless cold starts&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens between git push and production?
&lt;/h2&gt;

&lt;p&gt;A simplified pipeline 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;git push
  → clone repository
  → install dependencies
  → compile / transpile
  → bundle and optimise
  → run tests
  → create build artifact
  → deploy artifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part isn't the pipeline. It's what comes out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a build artifact?
&lt;/h2&gt;

&lt;p&gt;An artifact is the &lt;strong&gt;runnable output&lt;/strong&gt; of a build, frozen at one point in time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Typical artifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Containers&lt;/td&gt;
&lt;td&gt;Docker / OCI image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.next&lt;/code&gt; build output, or a static &lt;code&gt;out/&lt;/code&gt; folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;JAR or WAR file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless&lt;/td&gt;
&lt;td&gt;Zipped function package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static sites&lt;/td&gt;
&lt;td&gt;A folder of HTML, CSS and JS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's &lt;strong&gt;immutable&lt;/strong&gt;: once built, it never changes. If you want different behaviour, you build a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are rollbacks so fast?
&lt;/h2&gt;

&lt;p&gt;When you roll back on Vercel or Render, the platform usually &lt;strong&gt;doesn't rebuild anything&lt;/strong&gt;. It points traffic back at a previous artifact that already exists. Switching a pointer takes seconds, while a rebuild takes minutes.&lt;/p&gt;

&lt;p&gt;The same idea explains preview deployments: every branch gets its own artifact and its own URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does changing a dependency need a new deployment?
&lt;/h2&gt;

&lt;p&gt;Because you aren't editing a running application. A new dependency, a build setting, or (on many platforms) a build-time environment variable changes the inputs to the build, so you need a &lt;strong&gt;new artifact&lt;/strong&gt;. Runtime configuration is different, and is the subject of the next post: &lt;a href="https://aliahmed.si/blog/where-do-environment-variables-go/" rel="noopener noreferrer"&gt;where your environment variables actually go&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should this change how you think about CI/CD?
&lt;/h2&gt;

&lt;p&gt;Once this clicked for me, CI/CD stopped feeling like "automation" and started feeling like a &lt;strong&gt;manufacturing line&lt;/strong&gt;. Every commit goes in, and a production-ready artifact comes out. Three practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build once, promote everywhere.&lt;/strong&gt; The artifact that passed staging should be the exact artifact that reaches production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make builds reproducible.&lt;/strong&gt; Lockfiles, pinned base images and pinned tool versions mean the same commit gives the same artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag artifacts with the commit.&lt;/strong&gt; When something breaks, you should know exactly which code is running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're not deploying repositories anymore. We're deploying artifacts, and everything before that is the process of making them.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/what-gets-deployed-to-production/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>moderndeployments</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Securing a voice AI agent's webhook: what my first version got right, and what I'd change</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:06:21 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/securing-a-voice-ai-agents-webhook-what-my-first-version-got-right-and-what-id-change-5ge0</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/securing-a-voice-ai-agents-webhook-what-my-first-version-got-right-and-what-id-change-5ge0</guid>
      <description>&lt;p&gt;A voice AI agent's webhook receives &lt;strong&gt;phone numbers, transcripts and summaries of real customer calls&lt;/strong&gt;, so it needs three things: proof that the request came from the voice platform, a way to cope with the &lt;strong&gt;same call arriving several times&lt;/strong&gt;, and side effects (like emails) that happen &lt;strong&gt;exactly once&lt;/strong&gt;. &lt;strong&gt;HI Agent&lt;/strong&gt; is a 24/7 AI phone agent I built for home-service businesses on &lt;a href="https://vapi.ai/" rel="noopener noreferrer"&gt;Vapi&lt;/a&gt;. Its backend (&lt;a href="https://github.com/QuantumBreakz/Hi-Agent-Backend" rel="noopener noreferrer"&gt;source&lt;/a&gt;) gets those three mostly right. This post walks through how, and the two changes I'd make.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the backend do?
&lt;/h2&gt;

&lt;p&gt;When a call ends, Vapi sends a POST to &lt;code&gt;/webhook/vapi&lt;/code&gt;. The backend, Node.js and Express on Render:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checks the shared secret&lt;/strong&gt; Vapi sends in the &lt;code&gt;x-vapi-secret&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upserts a call log&lt;/strong&gt; in MongoDB Atlas, keyed by the call ID: phone number, duration, summary, transcript and the latest event type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emails the business owner&lt;/strong&gt; the summary and transcript through Resend, but only when the call is completely finished.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How do you know the request came from Vapi?
&lt;/h2&gt;

&lt;p&gt;Vapi lets you set a secret that it sends with every webhook. The middleware rejects requests before any work happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No header&lt;/strong&gt; → &lt;code&gt;401 Missing webhook signature&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong value&lt;/strong&gt; → &lt;code&gt;401 Invalid webhook signature&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No secret configured in production&lt;/strong&gt; → &lt;code&gt;500&lt;/code&gt;, so the endpoint &lt;strong&gt;fails closed&lt;/strong&gt; instead of accepting everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The secret lives in an environment variable on Render and in Vapi's webhook settings, never in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why upsert instead of insert?
&lt;/h2&gt;

&lt;p&gt;Vapi doesn't send one event per call. It sends several (status updates and then an &lt;code&gt;end-of-call-report&lt;/code&gt;), and any webhook can be retried. Inserting a row per request would leave you with duplicate, half-finished call logs.&lt;/p&gt;

&lt;p&gt;So the handler uses &lt;code&gt;findOneAndUpdate&lt;/code&gt; with &lt;code&gt;upsert: true&lt;/code&gt;, keyed by &lt;strong&gt;call ID&lt;/strong&gt;. Every event for a call updates the same document, and the stored &lt;code&gt;status&lt;/code&gt; always shows the latest event type. Retries and repeats converge on one correct record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you avoid sending five emails per call?
&lt;/h2&gt;

&lt;p&gt;The email goes out only when &lt;code&gt;message.type === "end-of-call-report"&lt;/code&gt;, the event that carries the final summary and full transcript. Status updates are saved but trigger nothing. The owner gets one email per call, with complete information.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would I change?
&lt;/h2&gt;

&lt;p&gt;Reviewing it now, two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Compare the secret in constant time.&lt;/strong&gt; The check uses &lt;code&gt;providedSecret !== secret&lt;/code&gt;. A plain string comparison can stop at the first different character, which in principle leaks timing information. Over the internet that's hard to exploit, but the fix costs nothing:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```ts title="Constant-time secret check"&lt;br&gt;
import { timingSafeEqual } from "node:crypto";&lt;/p&gt;

&lt;p&gt;const a = Buffer.from(providedSecret);&lt;br&gt;
const b = Buffer.from(secret);&lt;br&gt;
const ok = a.length === b.length &amp;amp;&amp;amp; timingSafeEqual(a, b);&lt;/p&gt;

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


**2. Remove the development bypass.** With no secret set and `NODE_ENV` not equal to `production`, the middleware skips validation entirely. That's convenient locally, but a staging deploy that forgets `NODE_ENV=production` becomes an open endpoint that writes to the database and sends email. Safer: always require a secret, and use a throwaway one locally. The repo already has a seed script that simulates a call, so local testing doesn't need the bypass.

A smaller improvement: the email step should also be idempotent. If Vapi retries the `end-of-call-report` after the email was sent but before the `200` arrived, the owner gets a second email. Recording `emailSentAt` on the call log and checking it first fixes that.

## The checklist for any AI agent webhook

* **Authenticate first**, in constant time, before parsing or storing anything.
* **Fail closed** when the secret is missing, in every environment.
* **Key storage by the platform's event or call ID** and upsert, so retries are harmless.
* **Trigger side effects on the final event only**, and record that they happened.
* **Treat transcripts as sensitive data:** they contain names, numbers and addresses.

The same lesson about authentication applies inside a single machine too: see [localhost is not authentication](https://aliahmed.si/blog/localhost-is-not-authentication/). For AI clients calling *into* your system, see [building an OAuth 2.1 MCP server](https://aliahmed.si/blog/oauth-mcp-server-crm/).

---

Written by Ali Ahmed · [Original article](https://aliahmed.si/blog/voice-ai-webhook-security/)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>aiagents</category>
      <category>voiceai</category>
      <category>security</category>
      <category>node</category>
    </item>
    <item>
      <title>How does Uber show cars moving in real time? A system design breakdown</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:05:45 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/how-does-uber-show-cars-moving-in-real-time-a-system-design-breakdown-a4m</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/how-does-uber-show-cars-moving-in-real-time-a-system-design-breakdown-a4m</guid>
      <description>&lt;p&gt;A ride-hailing app like Uber shows cars moving in real time by combining three ideas: &lt;strong&gt;push messaging&lt;/strong&gt; (the server sends updates only when something changes, instead of the app polling), &lt;strong&gt;spatial indexing with hexagons&lt;/strong&gt; (Uber's open-source &lt;strong&gt;H3&lt;/strong&gt; library, so a request only looks at drivers in nearby cells instead of every driver), and &lt;strong&gt;prediction plus correction&lt;/strong&gt; on the phone, which keeps the car icon gliding even when network updates arrive late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just poll for updates?
&lt;/h2&gt;

&lt;p&gt;The naive design has the app ask the server every few seconds: &lt;em&gt;"Any updates? How about now?"&lt;/em&gt; Uber's engineers have written about how this fell apart at scale. Most polling requests come back with nothing new, yet each one costs battery, bandwidth and server capacity, and on app launch they compete with the requests that matter.&lt;/p&gt;

&lt;p&gt;So Uber moved to &lt;strong&gt;push&lt;/strong&gt;. It built a real-time messaging platform, called &lt;strong&gt;RAMEN&lt;/strong&gt; in its engineering blog, that keeps a connection open to each app and sends updates when they exist. That platform decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt; to send: only on meaningful change. If a driver moves two metres, the rider doesn't need a message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; to send: a small diff, not the whole trip state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How&lt;/strong&gt; to send it: over a long-lived connection (Server-Sent Events at first, later gRPC streams).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do you find nearby drivers among millions?
&lt;/h2&gt;

&lt;p&gt;Computing the distance from a rider to every driver is fine with 100 drivers and hopeless with millions.&lt;/p&gt;

&lt;p&gt;Instead, the world is divided into cells. Uber open-sourced its grid as &lt;strong&gt;H3&lt;/strong&gt;, a hierarchical &lt;strong&gt;hexagonal&lt;/strong&gt; index. Every latitude and longitude maps to a cell ID at a chosen resolution. Drivers report their location, and the system keeps track of which cell each driver is in. To find drivers near a rider:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the rider's cell.&lt;/li&gt;
&lt;li&gt;Take that cell plus its ring of neighbours (&lt;code&gt;k&lt;/code&gt;-ring).&lt;/li&gt;
&lt;li&gt;Check only the drivers in those cells.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The search no longer scales with the total number of drivers on the platform, only with the number nearby.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hexagons instead of squares?
&lt;/h2&gt;

&lt;p&gt;In a square grid, a cell has 4 edge neighbours and 4 corner neighbours at different distances, so "one ring of neighbours" is a lumpy shape. In a hexagonal grid &lt;strong&gt;all 6 neighbours sit at the same distance&lt;/strong&gt; from the centre. Rings of hexagons approximate circles much better, which makes "everything within roughly X km" simple and fair.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do cars move smoothly with patchy data?
&lt;/h2&gt;

&lt;p&gt;Real networks drop packets and phones report GPS irregularly. If the app just jumped the icon to each new position, cars would teleport. Two standard techniques fix this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prediction (dead reckoning):&lt;/strong&gt; between updates, estimate where the car should be from its last speed, heading and the road it's on, and animate it there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correction (filtering):&lt;/strong&gt; when a real update arrives, blend the prediction with the measurement instead of snapping. A &lt;strong&gt;Kalman filter&lt;/strong&gt; is the classic tool: it weighs each source by how much it can be trusted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Snapping positions to the road network (map matching) removes the rest of the jitter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the lesson?
&lt;/h2&gt;

&lt;p&gt;"A car moving on a map" turns out to be real-time distributed messaging, event filtering, spatial indexing, predictive modelling and network optimisation, all working together quietly. &lt;strong&gt;The simplest user experiences are often powered by the most complex systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like system design breakdowns, read &lt;a href="https://aliahmed.si/blog/serverless-cold-starts-explained/" rel="noopener noreferrer"&gt;what a serverless cold start really is&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/uber-real-time-location-system/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>geospatial</category>
    </item>
    <item>
      <title>Your app doesn't run on a server anymore. It runs on a desired state.</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 21:43:27 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/your-app-doesnt-run-on-a-server-anymore-it-runs-on-a-desired-state-2g4o</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/your-app-doesnt-run-on-a-server-anymore-it-runs-on-a-desired-state-2g4o</guid>
      <description>&lt;p&gt;On a modern platform your application isn't attached to a particular server. You deploy a &lt;strong&gt;workload&lt;/strong&gt; and declare a &lt;strong&gt;desired state&lt;/strong&gt; ("keep three copies of this running"), and an orchestrator decides which machine runs it, moves it when a machine fails, and starts more copies when traffic grows. Servers still exist. They just stopped being the thing you deploy to.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 2 of the Modern Deployments series. Part 1: &lt;a href="https://aliahmed.si/blog/how-free-tiers-work/" rel="noopener noreferrer"&gt;how free tiers work&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Is "my app runs on an EC2 instance" wrong?
&lt;/h2&gt;

&lt;p&gt;I once heard someone say, "My app is running on an EC2 instance." It's not wrong. It's just not the whole story anymore.&lt;/p&gt;

&lt;p&gt;Take a typical deployment on Vercel, Render or Railway. You push code to GitHub. A build pipeline packages the application, often into a container image. The image goes to a registry. The platform schedules it somewhere in its fleet. At no point do you pick a machine, SSH into it, or even learn where your app ended up. That's intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did containers change deployment?
&lt;/h2&gt;

&lt;p&gt;Unlike a virtual machine, a container doesn't carry its own operating system. Containers on a host &lt;strong&gt;share the host's Linux kernel&lt;/strong&gt; and keep their own isolated processes, network and filesystem (through kernel features called namespaces and cgroups).&lt;/p&gt;

&lt;p&gt;That makes them light: they start in seconds or less, and many fit on one machine. A platform can move, restart or scale your app quickly because there is so little to boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the machine fails?
&lt;/h2&gt;

&lt;p&gt;Ten years ago, a dead host probably meant downtime and a pager going off.&lt;/p&gt;

&lt;p&gt;Today an orchestrator like &lt;strong&gt;Kubernetes&lt;/strong&gt; notices that the actual state (two copies running) no longer matches the desired state (three copies) and &lt;strong&gt;schedules a replacement on a healthy node&lt;/strong&gt;. Managed platforms run a similar control loop. You just never see it.&lt;/p&gt;

&lt;p&gt;That loop is the important idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You declare what should be true.&lt;/li&gt;
&lt;li&gt;The control plane keeps watching what &lt;em&gt;is&lt;/em&gt; true.&lt;/li&gt;
&lt;li&gt;When they differ, it acts until they match again.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What does this change for developers?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat instances as disposable.&lt;/strong&gt; Local disk and in-memory state can vanish when your app is moved. Put state in a database, object store or cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks matter.&lt;/strong&gt; The orchestrator decides what is "healthy" from your health endpoint, so make it honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale is configuration, not hardware.&lt;/strong&gt; Replicas and autoscaling rules replace buying a bigger box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which CPU runs your code is an implementation detail&lt;/strong&gt;, and it can change during the day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why &lt;a href="https://aliahmed.si/blog/serverless-cold-starts-explained/" rel="noopener noreferrer"&gt;cold starts&lt;/a&gt; exist: if the platform can place your workload anywhere, sometimes it has to create that place first.&lt;/p&gt;

&lt;p&gt;Next in the series: &lt;a href="https://aliahmed.si/blog/serverless-cold-starts-explained/" rel="noopener noreferrer"&gt;what is a serverless cold start?&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/servers-you-deploy-to/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>moderndeployments</category>
      <category>containers</category>
      <category>kubernetes</category>
      <category>cloud</category>
    </item>
    <item>
      <title>What is a serverless cold start, really?</title>
      <dc:creator>Ali Ahmed</dc:creator>
      <pubDate>Thu, 05 Nov 2026 21:42:51 +0000</pubDate>
      <link>https://dev.to/ali_ahmed_44ee99051e327ce/what-is-a-serverless-cold-start-really-4all</link>
      <guid>https://dev.to/ali_ahmed_44ee99051e327ce/what-is-a-serverless-cold-start-really-4all</guid>
      <description>&lt;p&gt;A &lt;strong&gt;cold start&lt;/strong&gt; is the extra time a serverless platform spends creating an execution environment before your code can handle a request: finding compute, starting an isolated sandbox, booting the language runtime, loading your code and running its top-level initialisation. A &lt;strong&gt;warm&lt;/strong&gt; request reuses an environment that already exists, which is why the first request after idle can take 2–3 seconds and the next one about 100 ms.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 3 of the Modern Deployments series. Previous: &lt;a href="https://aliahmed.si/blog/servers-you-deploy-to/" rel="noopener noreferrer"&gt;your app runs on a desired state&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the function "go to sleep"?
&lt;/h2&gt;

&lt;p&gt;That's the usual explanation, and it's a decent mental model, but it isn't what happens. Nothing sleeps. The platform &lt;strong&gt;reclaims&lt;/strong&gt; the environment. On a traditional server, your process is already running: memory allocated, runtime initialised, waiting for the next request. Serverless flips that. If a function hasn't been invoked for a while, the platform takes its CPU and memory back, because there is no reason to keep resources reserved for idle code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens during a cold start?
&lt;/h2&gt;

&lt;p&gt;When the next request arrives, your handler is not the first thing that runs. The platform has to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find available compute&lt;/strong&gt; in its fleet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create an isolated execution environment&lt;/strong&gt;: a container, a micro-VM (AWS Lambda uses Firecracker), or a V8 isolate on edge runtimes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start the language runtime&lt;/strong&gt;: Node.js, Python, the JVM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load your deployment artifact.&lt;/strong&gt; See &lt;a href="https://aliahmed.si/blog/what-gets-deployed-to-production/" rel="noopener noreferrer"&gt;what actually gets deployed&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run module-level initialisation&lt;/strong&gt;: imports, globals, SDK clients, config loading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invoke your handler.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of that is the cold start. On a warm invocation, steps 1–5 are skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does bundle size matter?
&lt;/h2&gt;

&lt;p&gt;Steps 4 and 5 grow with your code. The more you ship, and the more work you do at import time, the longer it takes before the first line of your handler runs. A function that imports a huge SDK to call one endpoint pays for the whole SDK on every cold start.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you reduce cold starts?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ship less code.&lt;/strong&gt; Tree-shake, bundle, and drop dependencies you don't use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initialise lazily.&lt;/strong&gt; Create expensive clients on first use, not at import time. Reuse them across warm invocations by keeping them in module scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a lighter runtime&lt;/strong&gt; where you can. Isolate-based edge runtimes start much faster than full containers, but support fewer APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep instances warm&lt;/strong&gt; with provisioned concurrency or minimum instances. This works, but you're now paying for idle compute again, which is the thing serverless was meant to avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the database close by.&lt;/strong&gt; A cold start next to a slow database connection handshake feels twice as bad.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Are cold starts a bug?
&lt;/h2&gt;

&lt;p&gt;No. They're the price of creating compute on demand instead of paying to keep it running. Once you understand the lifecycle, the behaviour stops being surprising, and you can decide case by case whether to optimise it away or pay for warmth.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://aliahmed.si/blog/what-gets-deployed-to-production/" rel="noopener noreferrer"&gt;what actually gets deployed to production?&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Written by Ali Ahmed · &lt;a href="https://aliahmed.si/blog/serverless-cold-starts-explained/" rel="noopener noreferrer"&gt;Original article&lt;/a&gt;&lt;/p&gt;

</description>
      <category>moderndeployments</category>
      <category>serverless</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
