<?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: Gaurang</title>
    <description>The latest articles on DEV Community by Gaurang (@gaurang_learn).</description>
    <link>https://dev.to/gaurang_learn</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%2F1500838%2Fac76d8ea-4994-4a80-b738-0dd848a78df7.png</url>
      <title>DEV Community: Gaurang</title>
      <link>https://dev.to/gaurang_learn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaurang_learn"/>
    <language>en</language>
    <item>
      <title>Your AI Agent Needs a Brake Pedal: Approval Gates, Scoped Permissions and Dry Runs</title>
      <dc:creator>Gaurang</dc:creator>
      <pubDate>Tue, 29 Sep 2026 04:49:02 +0000</pubDate>
      <link>https://dev.to/gaurang_learn/your-ai-agent-needs-a-brake-pedal-approval-gates-scoped-permissions-and-dry-runs-1hck</link>
      <guid>https://dev.to/gaurang_learn/your-ai-agent-needs-a-brake-pedal-approval-gates-scoped-permissions-and-dry-runs-1hck</guid>
      <description>&lt;p&gt;Agents are no longer only answering questions. They open PRs, run queries and send emails. Developer forums and regulators are now openly asking &lt;strong&gt;who is responsible when an agent does something nobody approved.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whatever the answer turns out to be, the engineering answer is the same: &lt;strong&gt;give real capability, but bound the blast radius.&lt;/strong&gt; Three patterns do most of that work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Approval gates: the agent proposes, a human decides
&lt;/h2&gt;

&lt;p&gt;The agent still reasons and plans. For &lt;em&gt;consequential&lt;/em&gt; tools, the final "actually do it" step waits for a 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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAgentLoopWithApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;consequentialTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                        &lt;span class="nx"&gt;callModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestHumanApproval&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;observations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;step&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="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&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;callModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;observations&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;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;final_answer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;decision&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;consequentialTools&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="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&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;approved&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;requestHumanApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// blocks&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;approved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rejected&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="na"&gt;reason&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 approved&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;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the agent sees the rejection and re-plans&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Don't gate everything.&lt;/strong&gt; An agent that asks permission for every search is slower than doing the task yourself. Gate the tools that send, write, delete or spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Scoped permissions: make the bad action impossible
&lt;/h2&gt;

&lt;p&gt;A gate depends on a human noticing a bad proposal. A &lt;strong&gt;scoped permission&lt;/strong&gt; means the bad action was never possible in the first place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A support agent's DB tool can only &lt;code&gt;SELECT&lt;/code&gt; &lt;strong&gt;this customer's&lt;/strong&gt; rows. No &lt;code&gt;DELETE&lt;/code&gt;, no other customers.&lt;/li&gt;
&lt;li&gt;A deploy agent's token can deploy to staging, not production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is plain least privilege, and it's also the strongest defense against &lt;strong&gt;prompt injection&lt;/strong&gt;, because an injected instruction can't call a capability the agent doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Dry runs: show the effect before committing
&lt;/h2&gt;

&lt;p&gt;A dry run executes the &lt;em&gt;planning&lt;/em&gt; logic without the side effect:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This would delete &lt;strong&gt;3&lt;/strong&gt; rows: &lt;code&gt;id IN (812, 813, 977)&lt;/code&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A gate asks &lt;em&gt;should this happen?&lt;/em&gt; A dry run answers &lt;em&gt;what exactly would happen?&lt;/em&gt; They work best together. Approving a concrete dry-run result is far more meaningful than approving "delete some records."&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the combination
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Guardrail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low stakes, reversible&lt;/td&gt;
&lt;td&gt;search, read-only lookup&lt;/td&gt;
&lt;td&gt;none needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moderate, harder to reverse&lt;/td&gt;
&lt;td&gt;email a customer, update a record&lt;/td&gt;
&lt;td&gt;approval gate + dry run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High stakes, irreversible&lt;/td&gt;
&lt;td&gt;payments, prod data deletion&lt;/td&gt;
&lt;td&gt;scoped permissions &lt;strong&gt;and&lt;/strong&gt; gate. Never the gate alone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the direct mitigations for &lt;strong&gt;"Excessive Agency"&lt;/strong&gt; in the OWASP Top 10 for LLM applications. It's the risk that grows fastest as agents get more capable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is from the AI/LLM Engineering pillar on &lt;a href="https://discoveringcode.in" rel="noopener noreferrer"&gt;discoveringCode&lt;/a&gt;, a free, ad-free notebook that goes from beginner to expert on AI/LLM engineering and four other pillars. Related: &lt;a href="https://discoveringcode.in/notebook/prompt-injection/" rel="noopener noreferrer"&gt;why prompt injection can't simply be patched&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>PDF Toolkit Where Your Files Can't Leave the Browser (and the CSP Enforces It)</title>
      <dc:creator>Gaurang</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:20:20 +0000</pubDate>
      <link>https://dev.to/gaurang_learn/pdf-toolkit-where-your-files-cant-leave-the-browser-and-the-csp-enforces-it-jgn</link>
      <guid>https://dev.to/gaurang_learn/pdf-toolkit-where-your-files-cant-leave-the-browser-and-the-csp-enforces-it-jgn</guid>
      <description>&lt;p&gt;Think about the last time you merged two PDFs online. You dragged a bank statement, a lease or a scan of your passport into a website, it "processed" the file, and you downloaded the result. &lt;strong&gt;Where did the file go in between?&lt;/strong&gt; Usually it went to someone else's server, and all you have is their word that they deleted it.&lt;/p&gt;

&lt;p&gt;I wanted a tool where that question has a boring answer, so I built &lt;a href="https://dokwise.in" rel="noopener noreferrer"&gt;Dokwise&lt;/a&gt;: 16 PDF and image tools (merge, split, compress, OCR, protect, compare, scan-to-PDF, e-signature and more) that run &lt;strong&gt;entirely in your browser&lt;/strong&gt;. There's no backend and no account, and after the first load it works offline.&lt;/p&gt;

&lt;p&gt;"We don't upload your files" is easy to say, though, and plenty of sites that say it are wrong. This post covers how Dokwise backs the claim up.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. There is no server to upload to
&lt;/h2&gt;

&lt;p&gt;Dokwise is a static site. There's no API and no file-processing server. You can't accidentally send a file to a backend that doesn't exist.&lt;/p&gt;

&lt;p&gt;The processing runs on libraries that work in the browser:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create / merge / split / edit PDFs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf-lib&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render, rasterize, extract text&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdfjs-dist&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image resize / compress / convert&lt;/td&gt;
&lt;td&gt;Canvas + &lt;code&gt;OffscreenCanvas&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tesseract.js&lt;/code&gt; (WASM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-256 PDF encryption&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@pdfsmaller/pdf-encrypt&lt;/code&gt; on top of native &lt;code&gt;crypto.subtle&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Heavy work runs in Web Workers and reports progress
&lt;/h2&gt;

&lt;p&gt;Each tool is a plain config object with a &lt;code&gt;process()&lt;/code&gt; function. The shell UI (drop zone, options, progress bar, download) is shared, so a new tool is just a config plus a worker. The worker receives &lt;code&gt;ArrayBuffer&lt;/code&gt;s, does its job, and &lt;strong&gt;transfers&lt;/strong&gt; the result back rather than copying 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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&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="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MergeRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;merge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&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;mergePdfs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;files&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;f&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bytes&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&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;buffer&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;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Any operation that can take more than a second has to post &lt;code&gt;{ type: 'progress', percent }&lt;/code&gt;.&lt;/strong&gt; That's a hard rule in the project. Compressing a 200-page PDF on a phone shouldn't mean staring at a spinner and wondering whether the tab has frozen.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The CSP turns the promise into an enforced rule
&lt;/h2&gt;

&lt;p&gt;This is the part I care most about. The production build injects a strict Content-Security-Policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;default&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
&lt;span class="n"&gt;script&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;www&lt;/span&gt;.&lt;span class="n"&gt;googletagmanager&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;;
&lt;span class="n"&gt;connect&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;www&lt;/span&gt;.&lt;span class="n"&gt;googletagmanager&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
            &lt;span class="n"&gt;https&lt;/span&gt;://*.&lt;span class="n"&gt;google&lt;/span&gt;-&lt;span class="n"&gt;analytics&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;://*.&lt;span class="n"&gt;analytics&lt;/span&gt;.&lt;span class="n"&gt;google&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;;
&lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;: &lt;span class="n"&gt;blob&lt;/span&gt;:;
&lt;span class="n"&gt;worker&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;:;
&lt;span class="n"&gt;object&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;;
&lt;span class="n"&gt;frame&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;fetch&lt;/code&gt;, XHR or WebSocket to a host that isn't listed &lt;strong&gt;gets blocked by the browser&lt;/strong&gt;. It doesn't matter if the call comes from my code or from a dependency buried in &lt;code&gt;node_modules&lt;/code&gt;. If a library quietly tries to phone home, it fails loudly instead of shipping silently.&lt;/p&gt;

&lt;p&gt;Being honest about it: the only third-party hosts on that list belong to Google Analytics, which gets anonymous page views. It never receives file contents or filenames. It's disclosed on the site's &lt;a href="https://dokwise.in/privacy" rel="noopener noreferrer"&gt;privacy page&lt;/a&gt;, and you can see it in the CSP above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't trust me, check it:&lt;/strong&gt; open DevTools → Network, run any tool, and watch what goes out. Your file won't be in there.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The OCR problem: tesseract.js calls a CDN by default
&lt;/h2&gt;

&lt;p&gt;The CSP caught a real problem here. Out of the box, &lt;code&gt;tesseract.js&lt;/code&gt; downloads its worker script, WASM core and language data &lt;strong&gt;from a CDN at runtime&lt;/strong&gt;. That's a third-party network call in the middle of processing your document, which is exactly what I'm trying to avoid.&lt;/p&gt;

&lt;p&gt;The fix was to vendor all of it (~30 MB) and point tesseract at same-origin paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&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;Tesseract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eng&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tesseract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OEM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LSTM_ONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;workerPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tessdata/worker.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;corePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tessdata/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;langPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tessdata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;gzip&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;So nobody pays 30 MB on every visit, the PWA service worker leaves &lt;code&gt;tessdata/&lt;/code&gt; out of the install-time precache and &lt;strong&gt;caches it the first time you actually run OCR&lt;/strong&gt;. After that, OCR works offline too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do with it today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Merge, split, organize (rotate / reorder / delete / crop pages)&lt;/li&gt;
&lt;li&gt;✅ Compress PDFs and images, convert JPG / PNG / WebP / HEIC&lt;/li&gt;
&lt;li&gt;✅ OCR scanned PDFs into searchable ones&lt;/li&gt;
&lt;li&gt;✅ Protect with AES-256, remove a password you know, strip metadata&lt;/li&gt;
&lt;li&gt;✅ Word ↔ PDF, compare two PDFs as a redline, repair broken PDFs&lt;/li&gt;
&lt;li&gt;✅ Scan to PDF with your camera, clean up a signature photo into a transparent PNG&lt;/li&gt;
&lt;li&gt;✅ Install it as a PWA and use it on a plane&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try it at &lt;a href="https://dokwise.in" rel="noopener noreferrer"&gt;dokwise.in&lt;/a&gt;.&lt;/strong&gt; I'd really like feedback from people who poke at it with DevTools open. If you spot a request that shouldn't be there, tell me.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with React, Vite, TypeScript, pdf-lib, pdfjs-dist and tesseract.js. The privacy page, with the full CSP and a DevTools walkthrough, is at &lt;a href="https://dokwise.in/privacy" rel="noopener noreferrer"&gt;dokwise.in/privacy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How a One-Line Column Rename Takes Down Production (and the Expand/Contract Fix)</title>
      <dc:creator>Gaurang</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:02:49 +0000</pubDate>
      <link>https://dev.to/gaurang_learn/how-a-one-line-column-rename-takes-down-production-and-the-expandcontract-fix-3fh</link>
      <guid>https://dev.to/gaurang_learn/how-a-one-line-column-rename-takes-down-production-and-the-expandcontract-fix-3fh</guid>
      <description>&lt;p&gt;Deploying new app code is the easy half. A rolling deploy swaps instances a few at a time, and for a few minutes old and new versions serve traffic side by side. That's fine; they're stateless.&lt;/p&gt;

&lt;p&gt;The hard half starts when the deploy &lt;strong&gt;also changes the database schema&lt;/strong&gt;. For those same minutes, &lt;strong&gt;both versions have to run correct queries against the same database.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-liner that breaks things
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&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;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;email_address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks trivial. But mid-deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old instances&lt;/strong&gt; still run &lt;code&gt;SELECT email FROM users&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New instances&lt;/strong&gt; already use &lt;code&gt;email_address&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment the migration runs, every request hitting an old instance fails with &lt;code&gt;column "email" does not exist&lt;/code&gt;. That's a real outage, even though the code on each side is correct on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: expand/contract
&lt;/h2&gt;

&lt;p&gt;Don't require the schema to be right at one single instant. Split one risky change into &lt;strong&gt;several safe ones&lt;/strong&gt;, each compatible with &lt;strong&gt;both&lt;/strong&gt; live versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1: Expand.&lt;/strong&gt; Add the new column, nullable:&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="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;email_address&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- no default, no table rewrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Old code doesn't know it exists, and nothing breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2: Dual-write.&lt;/strong&gt; Deploy code that writes &lt;em&gt;both&lt;/em&gt; columns but still reads the old one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`INSERT INTO users (name, email, email_address)
   VALUES ($1, $2, $2) RETURNING id, name, email`&lt;/span&gt;&lt;span class="p"&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="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stage 3: Backfill.&lt;/strong&gt; Copy old rows in &lt;strong&gt;batches&lt;/strong&gt;, not one giant &lt;code&gt;UPDATE&lt;/code&gt;:&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="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;email_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email_address&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- repeat per batch until none remain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stage 4: Switch reads.&lt;/strong&gt; Once every row has &lt;code&gt;email_address&lt;/code&gt;, deploy code that reads (and writes) only the new column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 5: Contract.&lt;/strong&gt; Once &lt;em&gt;every&lt;/em&gt; instance is on that version:&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="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;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's five steps instead of one &lt;code&gt;RENAME&lt;/code&gt;, and at no point is live code running a query the schema can't answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The one rule:&lt;/strong&gt; never require the schema and the live code to change at the same instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch your backfills.&lt;/strong&gt; One huge &lt;code&gt;UPDATE&lt;/code&gt; holds locks and floods the write-ahead log for its whole duration. Small batches take longer overall but don't compete with real traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-write is your rollback insurance.&lt;/strong&gt; If stage 4 goes wrong, rolling back to the stage-2 or stage-3 version still works, because it never stopped writing the old column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every change needs all five stages.&lt;/strong&gt; A purely &lt;em&gt;additive&lt;/em&gt; optional column is just stage 1. Save the full pattern for renames, type changes, column splits, or anything live code still depends on.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is the closing topic of the Infra &amp;amp; Delivery pillar on &lt;a href="https://discoveringcode.in" rel="noopener noreferrer"&gt;discoveringCode&lt;/a&gt;, a free, ad-free notebook that goes from SSH-ing into a box by hand to containerized CI/CD. Related: &lt;a href="https://discoveringcode.in/notebook/deployment-strategies-blue-green-canary-rolling/" rel="noopener noreferrer"&gt;deployment strategies: blue-green, canary, rolling&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>database</category>
      <category>postgres</category>
      <category>backend</category>
    </item>
    <item>
      <title>"How Many Servers Do We Need?" — Back-of-the-Envelope Estimation for System Design</title>
      <dc:creator>Gaurang</dc:creator>
      <pubDate>Sun, 20 Sep 2026 05:07:06 +0000</pubDate>
      <link>https://dev.to/gaurang_learn/how-many-servers-do-we-need-back-of-the-envelope-estimation-for-system-design-2h6m</link>
      <guid>https://dev.to/gaurang_learn/how-many-servers-do-we-need-back-of-the-envelope-estimation-for-system-design-2h6m</guid>
      <description>&lt;p&gt;"How many servers do we need?" sounds like a question that needs a research project to answer. It doesn't — it needs five minutes of napkin math.&lt;/p&gt;

&lt;p&gt;Back-of-the-envelope estimation is the habit of turning vague requirements ("we'll have a lot of users") into concrete numbers (queries per second, gigabytes of storage, megabits of bandwidth) before you design anything, because every real decision downstream — one database or many, a cache or not, which scaling strategy — depends on roughly knowing the size of the problem first.&lt;/p&gt;

&lt;p&gt;The point is never precision. Nobody expects an exact server count from a five-minute calculation. The point is the right &lt;strong&gt;order of magnitude&lt;/strong&gt; — knowing whether you're building for 100 requests a second or 100,000 completely changes the architecture, and that distinction is visible from rough math alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A link-sharing app, 10 million daily active users

users posting a link:        1% of DAU post per day  = 100,000 posts/day
average post size:           ~1 KB (link + metadata)
daily write volume:          100,000 × 1KB            = ~100 MB/day
write QPS (spread evenly):   100,000 / 86,400 sec      ≈ 1.2 writes/sec
                                                          (but real traffic
                                                           isn't even — peak
                                                           is often 3-5x avg)
peak write QPS:               ~1.2 × 4                 ≈ 5 writes/sec

reads (10:1 read:write ratio, typical for social content):
peak read QPS:                 5 × 10                  ≈ 50 reads/sec

storage for 5 years of posts: 100 MB/day × 365 × 5     ≈ 180 GB
                                                          (comfortably fits
                                                          on one database)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifty reads a second and a couple hundred gigabytes of data is a completely different design conversation than fifty &lt;em&gt;thousand&lt;/em&gt; reads a second and a couple hundred &lt;em&gt;terabytes&lt;/em&gt; — and that difference took one minute of multiplication to surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference numbers worth memorizing
&lt;/h2&gt;

&lt;p&gt;A handful of reference numbers make this fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single reasonably-sized application server can typically handle somewhere from a few hundred to a few thousand requests per second, depending heavily on what each request does — simple reads land higher, requests that hit a database or do real computation land lower.&lt;/li&gt;
&lt;li&gt;A relational database on solid hardware handles thousands of simple queries per second.&lt;/li&gt;
&lt;li&gt;A network round trip within the same data center costs roughly 0.5–1ms; a round trip across the country or across an ocean costs 50–150ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these need to be exact — knowing "a server handles low thousands of req/sec, not millions" is enough to sanity-check any estimate you produce.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few rules that make this fast
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Work in the right units, and convert only at the end.&lt;/strong&gt; Traffic is usually easiest to reason about in requests per day (from a DAU number) and then converted to requests per second by dividing by 86,400 — but daily active users don't spread their activity evenly across 24 hours. Real traffic has a peak-to-average ratio, commonly somewhere around 2–5x during a system's busiest hour, and any capacity plan built only on the &lt;em&gt;average&lt;/em&gt; QPS will fall over during the actual peak, which is precisely when it matters most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage math almost always dominates in surprising ways.&lt;/strong&gt; A feature that feels "small" per item — a 280-character post, a single log line — adds up fast at scale: a million events a day, each 1KB, is a gigabyte a day, 365GB a year, without a single image or video involved. This is usually the number that first forces a "do we need object storage for this" or "do we need to shard this table" conversation, well before raw request volume does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round aggressively.&lt;/strong&gt; 10 million users, not 9.7 million. 1KB per post, not 847 bytes. The entire value of this exercise is speed and directional correctness — spending ten minutes getting a number precise to three significant figures defeats the purpose, when the actual decision only cares whether you're closer to a hundred, a hundred thousand, or a hundred million.&lt;/p&gt;

&lt;p&gt;This estimate becomes the shared vocabulary for every trade-off later in a system design discussion. "Do we need to shard the database" is unanswerable in the abstract, but very answerable once you know you're expecting 50,000 writes/sec against a single table. "Do we need a CDN" is unanswerable in the abstract, but obvious once you know 80% of your users are outside the region your one server lives in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is one topic in the System Design pillar on &lt;a href="https://discoveringcode.in" rel="noopener noreferrer"&gt;discoveringCode&lt;/a&gt; — a free, ad-free notebook that goes beginner-to-expert on frontend, backend, system design, infra, and AI/LLM engineering. Next up in the pillar: &lt;a href="https://discoveringcode.in/notebook/availability-reliability-and-the-nines/" rel="noopener noreferrer"&gt;Availability, Reliability &amp;amp; the Nines&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>interview</category>
      <category>computerscience</category>
      <category>career</category>
    </item>
    <item>
      <title>What an LLM Actually Is (and Isn't)</title>
      <dc:creator>Gaurang</dc:creator>
      <pubDate>Fri, 18 Sep 2026 16:17:23 +0000</pubDate>
      <link>https://dev.to/gaurang_learn/what-an-llm-actually-is-and-isnt-5e0a</link>
      <guid>https://dev.to/gaurang_learn/what-an-llm-actually-is-and-isnt-5e0a</guid>
      <description>&lt;p&gt;An LLM doesn't "know" that Paris is the capital of France the way a database row knows it. It doesn't understand your question, and it isn't reasoning before it answers.&lt;/p&gt;

&lt;p&gt;Here's the one sentence that explains almost everything "weird" about how these models behave:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A large language model is a function that takes in some text and outputs a probability distribution over what token is most likely to come &lt;em&gt;next&lt;/em&gt;. That's it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It has seen so much text where "The capital of France is" is followed by "Paris" that "Paris" gets an overwhelmingly high probability in that spot. Ask it something its training data genuinely didn't cover well, and it still has to output &lt;em&gt;something&lt;/em&gt; with high probability — which is the actual mechanism behind hallucination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next-token prediction, made concrete
&lt;/h2&gt;

&lt;p&gt;Feed the model a sequence of tokens (words and word-pieces). The model outputs a probability for &lt;em&gt;every&lt;/em&gt; token in its vocabulary being the next one. A sampling step picks one, appends it, and the whole thing runs again to predict the token after &lt;em&gt;that&lt;/em&gt; — one token at a time, until it hits a stop token or a length limit.&lt;/p&gt;

&lt;p&gt;Here's a wildly simplified stand-in for the mechanism (real models score tens of thousands of candidate tokens with a neural network; this hardcodes a tiny probability table just to make the shape of it visible):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextTokenProbabilities&lt;/span&gt; &lt;span class="o"&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;The capital of France is&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;Paris&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Lyon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;France&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.02&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="mf"&gt;0.01&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2 + 2 =&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;22&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.01&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;predictNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promptSoFar&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;distribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextTokenProbabilities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;promptSoFar&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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="mf"&gt;1.0&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;bestToken&lt;/span&gt;&lt;span class="p"&gt;]&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;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distribution&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&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;0&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;bestToken&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="nf"&gt;predictNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The capital of France is&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "Paris" — highest probability, not "known fact"&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="nf"&gt;predictNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2 + 2 =&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                   &lt;span class="c1"&gt;// "4" — same mechanism, correct answer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: nothing in this function &lt;em&gt;checks&lt;/em&gt; an answer against reality. Both correct outputs came from the identical process — "what token has the highest probability given everything before it" — as any hallucinated output would. There's no separate "is this true" step. That's not a bug you patch; it's the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this rules out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No understanding, no reasoning "before" answering&lt;/strong&gt;, in the way those words imply for a person. The model isn't silently forming an opinion and then describing it — the text &lt;em&gt;is&lt;/em&gt; the computation. It's also why chain-of-thought prompting works at all: forcing the model to generate intermediate reasoning tokens genuinely changes what gets predicted next, because each new token becomes part of what the &lt;em&gt;next&lt;/em&gt; prediction conditions on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No live knowledge of anything after its training cutoff&lt;/strong&gt;, and no access to your data, by default. Ask a base model what today's date is, and it can't know. Closing that gap is the entire premise of retrieval (RAG) and tool calling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No persistent memory between separate API calls.&lt;/strong&gt; Every request is stateless — "remembering" a conversation is really "re-sending the whole conversation so far," which is exactly why context windows become a hard, practical constraint the moment a conversation runs long.&lt;/p&gt;

&lt;p&gt;Keep this mental model — text in, next-token probabilities out, sampled and repeated — in mind for everything else: tokenization, context windows, hallucination, prompting techniques, and even why agents get stuck in loops all trace back to this one mechanism.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the opening topic of the AI/LLM Engineering pillar on &lt;a href="https://discoveringcode.in" rel="noopener noreferrer"&gt;discoveringCode&lt;/a&gt; — a free, ad-free notebook that goes beginner-to-expert on frontend, backend, system design, infra, and AI/LLM engineering, with live interactive code playgrounds. If this was useful, the &lt;a href="https://discoveringcode.in/notebook/tokens-and-tokenization/" rel="noopener noreferrer"&gt;next topic on tokenization&lt;/a&gt; builds directly on it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>beginners</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
