<?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: Syed Anzar</title>
    <description>The latest articles on DEV Community by Syed Anzar (@syed_anzar).</description>
    <link>https://dev.to/syed_anzar</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%2F4069010%2Fda8f710a-f285-4309-8c69-770badbe9c7b.jpg</url>
      <title>DEV Community: Syed Anzar</title>
      <link>https://dev.to/syed_anzar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syed_anzar"/>
    <language>en</language>
    <item>
      <title>Why Increasing Database Connections Makes Queries Slower: Process Overhead, Little’s Law, and Pool Sizing Math</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:34:25 +0000</pubDate>
      <link>https://dev.to/syed_anzar/why-increasing-database-connections-makes-queries-slower-process-overhead-littles-law-and-pool-57jn</link>
      <guid>https://dev.to/syed_anzar/why-increasing-database-connections-makes-queries-slower-process-overhead-littles-law-and-pool-57jn</guid>
      <description>&lt;p&gt;When an application experiences traffic spikes and database latency rises, the instinctive reaction for many engineering teams is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Our queries are queuing up. Let's increase &lt;code&gt;max_connections&lt;/code&gt; on PostgreSQL from 100 to 1,000 and bump our ORM connection pool size."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within minutes of deploying that change, query latency explodes from 5ms to 4,000ms. Database CPU utilization hits 100%, memory usage spikes, and application pods start crashing with connection timeout errors.&lt;/p&gt;

&lt;p&gt;Adding database connections under heavy load does not increase concurrency. In most production systems, it triggers a catastrophic collapse in throughput. &lt;/p&gt;

&lt;p&gt;Here is what actually happens inside the Linux kernel, PostgreSQL's process table, and hardware CPU caches when you over-allocate database connections, alongside the exact math to size your pools correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Hardware Reality: CPU Cores Can Only Execute One Thing at a Time
&lt;/h2&gt;

&lt;p&gt;A common misconception among developers is that an active connection represents concurrent query execution.&lt;/p&gt;

&lt;p&gt;An 8-core CPU can only execute 8 instructions at any given nanosecond. If your database server has 8 physical cores and receives 500 active queries across 500 open connections, exactly 8 queries are being evaluated by the CPU. The remaining 492 queries are sitting in OS run queues waiting for CPU time slices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       500 Client Connections
                 │
                 ▼
┌───────────────────────────────────┐
│     OS Thread / Process Queue     │
│   (492 Backends Waiting / Blocked) │
└─────────────────┬─────────────────┘
                  │ Context Switch Storm
                  ▼
         [ 8 Physical Cores ]
   (Evaluating 8 Queries at a Time)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the operating system scheduler attempts to juggle hundreds of runnable processes across a handful of physical cores, it enters a state known as &lt;strong&gt;context-switching thrashing&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;State Preservation Overhead:&lt;/strong&gt; For every context switch, the kernel must save CPU register states, stack pointers, and program counters to memory, then restore the state of the incoming process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation Lookaside Buffer (TLB) Invalidation:&lt;/strong&gt; Because PostgreSQL uses separate processes rather than lightweight threads, switching between backend processes forces the CPU to switch page directory tables, invalidating the TLB cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L1/L2/L3 Cache Eviction:&lt;/strong&gt; Query execution engines rely heavily on CPU caches for parse trees, index root pages, and sort buffers. Constant process switching flushes the cache lines, forcing the CPU to fetch data repeatedly from slower system RAM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At 500+ active connections on an 8-core machine, the CPU spends more clock cycles managing process scheduling and cache misses than executing SQL logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The PostgreSQL Process-per-Connection Architecture
&lt;/h2&gt;

&lt;p&gt;Unlike MySQL (which assigns a lightweight thread to each connection) or Redis (which uses an asynchronous event loop), PostgreSQL uses a &lt;strong&gt;process-per-connection&lt;/strong&gt; model.&lt;/p&gt;

&lt;p&gt;When a client establishes a connection, the PostgreSQL supervisor daemon (&lt;code&gt;postmaster&lt;/code&gt;) invokes the &lt;code&gt;fork()&lt;/code&gt; system call to spawn an independent OS process:&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="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;54322&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architectural choice offers immense memory isolation and fault tolerance (a crashing worker cannot corrupt other backends), but it imposes severe per-connection costs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               ┌────────────────────────┐
               │   Postmaster Daemon    │
               └───────────┬────────────┘
                           │ fork()
            ┌──────────────┼──────────────┐
            ▼              ▼              ▼
     ┌─────────────┐┌─────────────┐┌─────────────┐
     │ Backend #1  ││ Backend #2  ││ Backend #N  │
     │ Private RSS ││ Private RSS ││ Private RSS │
     │  5-10 MB    ││  5-10 MB    ││  5-10 MB    │
     └──────┬──────┘└──────┬──────┘└──────┬──────┘
            │              │              │
            └──────────────┼──────────────┘
                           ▼
     ┌───────────────────────────────────────────┐
     │          Shared Memory Segment            │
     │  (shared_buffers, ProcArray, WAL Buffers) │
     └───────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A. Private Memory (RSS) Footprint
&lt;/h3&gt;

&lt;p&gt;Every PostgreSQL backend process maintains its own private address space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base Process Memory:&lt;/strong&gt; Between 5 MB and 10 MB of RSS for process stacks, catalog caches, and local memory contexts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;work_mem&lt;/code&gt; Dynamic Allocation:&lt;/strong&gt; The &lt;code&gt;work_mem&lt;/code&gt; setting (default 4 MB) is allocated &lt;strong&gt;per sort or hash operation node&lt;/strong&gt; in a query plan, not per connection. A complex query performing two joins, an aggregation, and an &lt;code&gt;ORDER BY&lt;/code&gt; can allocate 4 separate &lt;code&gt;work_mem&lt;/code&gt; chunks:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\text{Query Memory} = 4 \times 4\,\text{MB} = 16\,\text{MB}$$&lt;/p&gt;

&lt;p&gt;If 300 active connections execute similar reporting or join queries concurrently:&lt;/p&gt;

&lt;p&gt;$$\text{Total Memory Demand} = 300 \times 16\,\text{MB} = 4.8\,\text{GB}$$&lt;/p&gt;

&lt;p&gt;This memory is allocated outside &lt;code&gt;shared_buffers&lt;/code&gt;. When combined with base RSS, unconstrained connection counts quickly trigger the Linux Out-Of-Memory (OOM) Killer, terminating the PostgreSQL master process.&lt;/p&gt;

&lt;h3&gt;
  
  
  B. Shared Memory Contention: The &lt;code&gt;ProcArrayLock&lt;/code&gt; Bottleneck
&lt;/h3&gt;

&lt;p&gt;To provide Multi-Version Concurrency Control (MVCC) snapshot isolation, PostgreSQL backends must track the status of all active transactions.&lt;/p&gt;

&lt;p&gt;This tracking happens in a central shared-memory structure called &lt;code&gt;ProcArray&lt;/code&gt; (an array of &lt;code&gt;PGPROC&lt;/code&gt; structures). Whenever any transaction begins, commits, or aborts, it must acquire an exclusive or shared lock on &lt;code&gt;ProcArrayLock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With 20 connections, acquiring &lt;code&gt;ProcArrayLock&lt;/code&gt; is instantaneous. With 1,000 connections, hundreds of CPU cores and threads spend thousands of clock cycles spinning on futexes and spinlocks just trying to register transaction snapshots.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Queueing Math: Little's Law
&lt;/h2&gt;

&lt;p&gt;Queueing theory proves why adding connections slows down your database. &lt;strong&gt;Little's Law&lt;/strong&gt; defines the mathematical relationship between concurrency, throughput, and latency in any stable system:&lt;/p&gt;

&lt;p&gt;$$L = \lambda \times W$$&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$L$&lt;/strong&gt; = Average number of concurrent requests in the system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$\lambda$&lt;/strong&gt; = System throughput (arrival rate in queries per second).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$W$&lt;/strong&gt; = Average time spent processing each query (latency in seconds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let us apply real numbers. Suppose your API requires your database to process &lt;strong&gt;2,000 queries per second (QPS)&lt;/strong&gt;, and your database indexes are well-tuned such that the average query takes &lt;strong&gt;5 milliseconds (0.005s)&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;$$L = 2000 \times 0.005 = 10$$&lt;/p&gt;

&lt;p&gt;You only need &lt;strong&gt;10 concurrent connections&lt;/strong&gt; to sustain a throughput of 2,000 QPS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming: 2,000 QPS ──▶ [ 10 Active DB Connections ] ──▶ Completed in 5ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Happens When You Add 200 Connections?
&lt;/h3&gt;

&lt;p&gt;If your database hardware can only execute 10 queries concurrently without CPU contention, opening 200 connections does not increase $\lambda$ (throughput). The physical disk and CPU remain bounded.&lt;/p&gt;

&lt;p&gt;Instead, the extra connections increase $W$ (latency) because queries spend time waiting in OS run queues and lock barriers:&lt;/p&gt;

&lt;p&gt;$$W = \frac{L}{\lambda} = \frac{200}{2000} = 0.100\,\text{seconds} = 100\,\text{ms}$$&lt;/p&gt;

&lt;p&gt;By opening 200 connections to solve a perceived capacity problem, query latency increases by &lt;strong&gt;20x&lt;/strong&gt; (from 5ms to 100ms) with zero gain in throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Canonical Connection Pool Formula
&lt;/h2&gt;

&lt;p&gt;The PostgreSQL development team and the authors of HikariCP established an empirical formula for optimal database connection pool sizing:&lt;/p&gt;

&lt;p&gt;$$\text{Pool Size} = (\text{Core Count} \times 2) + \text{Effective Spindle Count}$$&lt;/p&gt;

&lt;p&gt;Let us break down each term:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;$\text{Core Count} \times 2$:&lt;/strong&gt; When one query briefly stalls waiting for a page fault or synchronous disk I/O, the operating system can seamlessly switch the CPU core to execute a second active query without idle CPU bubbles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$\text{Effective Spindle Count}$:&lt;/strong&gt; In traditional spinning disks (HDDs), each physical spindle could seek independently. On modern NVMe SSDs where active working sets are cached in RAM or &lt;code&gt;shared_buffers&lt;/code&gt;, effective spindle count is &lt;strong&gt;1&lt;/strong&gt; (or approaching &lt;strong&gt;0&lt;/strong&gt; for 100% in-memory datasets).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Practical Calculation
&lt;/h3&gt;

&lt;p&gt;For a standard cloud instance with an 8-core CPU running on NVMe storage:&lt;/p&gt;

&lt;p&gt;$$\text{Optimal Pool Size} = (8 \times 2) + 1 = 17\,\text{connections}$$&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hardware Specs       Formula Calculation        Optimal Connections
───────────────────────────────────────────────────────────────────
4 CPU Cores, SSD     (4 * 2) + 1                9 Connections
8 CPU Cores, SSD     (8 * 2) + 1                17 Connections
16 CPU Cores, SSD    (16 * 2) + 1               33 Connections
32 CPU Cores, SSD    (32 * 2) + 1               65 Connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running benchmarks (such as &lt;code&gt;pgbench&lt;/code&gt;) on an 8-core server shows that throughput peaks between 16 and 25 connections. Pushing connections beyond 100 causes throughput to drop steeply while response latency rises exponentially.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Kubernetes Multi-Pod Multiplication Mistake
&lt;/h2&gt;

&lt;p&gt;In containerized microservices architectures, default connection settings cause accidental database overload.&lt;/p&gt;

&lt;p&gt;Consider a standard setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 Kubernetes application pods running Spring Boot, Node.js, or Go.&lt;/li&gt;
&lt;li&gt;Default ORM / pool config: &lt;code&gt;maximumPoolSize = 20&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 Pods x 20 Connections/Pod = 200 Total Connections to PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When traffic spikes and Kubernetes Horizontal Pod Autoscaler (HPA) scales the deployment from 10 pods to 30 pods, the connection count surges to &lt;strong&gt;600 connections&lt;/strong&gt;. The database crashes precisely when user demand is highest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       [ Pod 1 (20) ]
       [ Pod 2 (20) ] ──▶ Total: 200 to 600 Connections ──▶ [ 8-Core Postgres ]
       [ Pod N (20) ]                                        (Overloaded &amp;amp; Crashing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Correct Formula for Distributed Pods
&lt;/h3&gt;

&lt;p&gt;To prevent pool bloat across multiple application instances, allocate connection quotas proportionally:&lt;/p&gt;

&lt;p&gt;$$\text{Pool Per Pod} = \max\left(2, \; \left\lfloor \frac{\text{Target DB Connections}}{\text{Number of Pods}} \right\rfloor\right)$$&lt;/p&gt;

&lt;p&gt;If your 16-core database supports a target pool of 34 connections across 10 application pods:&lt;/p&gt;

&lt;p&gt;$$\text{Pool Per Pod} = \left\lfloor \frac{34}{10} \right\rfloor = 3\,\text{connections per pod}$$&lt;/p&gt;

&lt;p&gt;Each pod needs only 3 to 4 connections if queries are fast and transactions are short.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Client Pools vs Middleware Proxies (PgBouncer)
&lt;/h2&gt;

&lt;p&gt;When you have hundreds of microservices, serverless functions (like AWS Lambda), or edge workers, dividing 20 connections across 500 callers is impossible.&lt;/p&gt;

&lt;p&gt;This is where connection pooling layers differ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────────────────────────────┐
│                    Connection Management                      │
├───────────────────────────────┬───────────────────────────────┤
│ Client-Side Pooling           │ Middleware Proxy Pooling      │
│ (HikariCP, pgxpool, Prisma)   │ (PgBouncer, Odyssey, Pgpool)  │
├───────────────────────────────┼───────────────────────────────┤
│ Operates inside app process   │ Standalone proxy server       │
│ Cannot coordinate across pods │ Centralized queueing for all  │
│ High idle connection overhead │ Ultra-lightweight (2KB / conn)│
│ Best for fixed monorepos      │ Mandatory for serverless/K8s  │
└───────────────────────────────┴───────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why PgBouncer in Transaction Mode Solves the Problem
&lt;/h3&gt;

&lt;p&gt;PgBouncer acts as a lightweight proxy between your application fleet and PostgreSQL. Built on a single-threaded asynchronous &lt;code&gt;epoll&lt;/code&gt; event loop, PgBouncer can maintain &lt;strong&gt;10,000 idle client connections&lt;/strong&gt; with negligible memory usage (~2 KB per connection).&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Transaction Pooling Mode&lt;/strong&gt; (&lt;code&gt;pool_mode = transaction&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A client connects to PgBouncer. PgBouncer accepts the TCP connection immediately.&lt;/li&gt;
&lt;li&gt;No PostgreSQL backend process is assigned while the client is idle (e.g., executing application business logic or external API calls).&lt;/li&gt;
&lt;li&gt;The moment the client issues &lt;code&gt;BEGIN&lt;/code&gt; or a standalone SQL query, PgBouncer binds an active PostgreSQL server connection from its small pool of 20 backends.&lt;/li&gt;
&lt;li&gt;As soon as the client issues &lt;code&gt;COMMIT&lt;/code&gt; or &lt;code&gt;ROLLBACK&lt;/code&gt;, PgBouncer reclaims the PostgreSQL backend and hands it to another waiting client.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ 10,000 Client Apps / Lambdas ]
              │ (Lightweight TCP, 2 KB RAM each)
              ▼
    [ PgBouncer (pool_mode=transaction) ]
              │ (Strictly 20 Server Connections)
              ▼
    [ PostgreSQL (8-Core Server) ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Transaction Pooling Incompatibilities to Watch Out For
&lt;/h3&gt;

&lt;p&gt;Because transaction pooling reassigns the underlying PostgreSQL connection after every transaction, session-scoped features will not persist across transactions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Variables:&lt;/strong&gt; &lt;code&gt;SET TIME ZONE&lt;/code&gt; or &lt;code&gt;SET search_path&lt;/code&gt; will affect whichever backend process ran the command, leaking state to subsequent clients. Use &lt;code&gt;SET LOCAL&lt;/code&gt; within transaction blocks instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LISTEN&lt;/code&gt; / &lt;code&gt;NOTIFY&lt;/code&gt;:&lt;/strong&gt; Requires a dedicated long-lived connection (&lt;code&gt;pool_mode = session&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepared Statements:&lt;/strong&gt; Historically broken in transaction pooling. Since PgBouncer 1.21+, you can enable protocol-level prepared statement tracking by configuring:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;;; pgbouncer.ini
&lt;/span&gt;&lt;span class="py"&gt;max_prepared_statements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Diagnosing Connection Health in Production
&lt;/h2&gt;

&lt;p&gt;To inspect how connections are behaving on your PostgreSQL instance, run this diagnostic query:&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;SELECT&lt;/span&gt; 
    &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;connection_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;state_change&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_seconds_in_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;state_change&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_seconds_in_state&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pg_backend_pid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;connection_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to Interpret the Output:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;idle in transaction&lt;/code&gt; (Danger):&lt;/strong&gt; Application code opened a transaction (&lt;code&gt;BEGIN&lt;/code&gt;), executed a query, and stalled waiting for HTTP calls or internal processing before issuing &lt;code&gt;COMMIT&lt;/code&gt;. This holds locks and blocks VACUUM cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;idle&lt;/code&gt; (Waste):&lt;/strong&gt; Connections are established and sitting unused. If &lt;code&gt;idle&lt;/code&gt; connections exceed 80% of &lt;code&gt;max_connections&lt;/code&gt;, lower your application pool sizes or introduce PgBouncer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;active&lt;/code&gt; (Executing):&lt;/strong&gt; If &lt;code&gt;active&lt;/code&gt; connections consistently exceed &lt;code&gt;(CPU Cores * 2)&lt;/code&gt;, queries are waiting on locks, slow disk scans, or missing indexes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. Summary: Senior Engineer Sizing Rules
&lt;/h2&gt;

&lt;p&gt;When designing connection architectures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep database pools small:&lt;/strong&gt; A pool size of &lt;code&gt;(Cores * 2) + 1&lt;/code&gt; maximizes CPU cache efficiency and minimizes scheduling thrashing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue in the application, not the database:&lt;/strong&gt; Let application threads or connection poolers hold waiting requests in memory rather than forcing PostgreSQL to fork heavy processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard against pod scaling:&lt;/strong&gt; Ensure &lt;code&gt;(Pods * Pool Size)&lt;/code&gt; never exceeds your database core limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use PgBouncer for high concurrency:&lt;/strong&gt; If your architecture requires hundreds of client connections (microservices, serverless), place PgBouncer in front of PostgreSQL in transaction pooling mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep transactions short:&lt;/strong&gt; Never perform network I/O, file operations, or heavy computational tasks inside an open database transaction block.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>performance</category>
      <category>systems</category>
    </item>
    <item>
      <title>Why UPDATE Is Secretly an INSERT: How Postgres MVCC, xmin/xmax, and Table Bloat Actually Work</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Fri, 09 Oct 2026 04:59:50 +0000</pubDate>
      <link>https://dev.to/syed_anzar/why-update-is-secretly-an-insert-how-postgres-mvcc-xminxmax-and-table-bloat-actually-work-3514</link>
      <guid>https://dev.to/syed_anzar/why-update-is-secretly-an-insert-how-postgres-mvcc-xminxmax-and-table-bloat-actually-work-3514</guid>
      <description>&lt;p&gt;When you run an &lt;code&gt;UPDATE&lt;/code&gt; statement in PostgreSQL, your intuition tells you something simple happens: the database finds the row on disk, seeks to the modified column, and overwrites the old bytes with the new values.&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Postgres actually worked this way, concurrent reads would crash, transactions would read half-written dirty bytes, and rollbacks would require expensive undo operations.&lt;/p&gt;

&lt;p&gt;Postgres never modifies a row in place during an &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, Postgres performs an &lt;code&gt;INSERT&lt;/code&gt; followed by a soft &lt;code&gt;DELETE&lt;/code&gt;. It writes a completely new physical copy of the row, leaves the old row untouched on disk, marks the old row as expired, and links them together.&lt;/p&gt;

&lt;p&gt;Understanding this mechanism (Multi-Version Concurrency Control, or MVCC) is the key to understanding why write-heavy Postgres databases experience massive table bloat, why secondary indexes suddenly degrade, and how Postgres engineers designed Heap-Only Tuples (HOT) to survive it.&lt;/p&gt;

&lt;p&gt;Let us look beneath the SQL layer into the physical 8KB page layout, tuple headers, and transaction visibility rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Physical Storage: Inside the 8KB Heap Page
&lt;/h2&gt;

&lt;p&gt;To see what happens during an update, you first need to see how Postgres stores data on disk.&lt;/p&gt;

&lt;p&gt;Postgres tables are divided into physical 8KB blocks called pages (or buffers in shared memory). Each page is organized from the outside in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
| PageHeaderData (24 bytes)                                             |
| pd_lsn | pd_flags | pd_lower | pd_upper | pd_special ...              |
+-----------------------------------------------------------------------+
| ItemId (Line Pointers array: 4 bytes each, grows DOWNWARDS)           |
| [Line Pointer 1] -&amp;gt; offset to Tuple 1                                 |
| [Line Pointer 2] -&amp;gt; offset to Tuple 2                                 |
|                    | (Free Space Gap) |                               |
|                    v                  v                               |
+-----------------------------------------------------------------------+
| Heap Tuples (Actual Row Data: grows UPWARDS from the bottom)          |
|                                                                       |
| [Tuple 2 Payload + Header (23 bytes)]                                 |
| [Tuple 1 Payload + Header (23 bytes)]                                 |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every physical tuple on a page has a 23-byte header (&lt;code&gt;HeapTupleHeaderData&lt;/code&gt;) containing hidden metadata columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;t_xmin&lt;/code&gt;: The Transaction ID (XID) of the transaction that created/inserted this row version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t_xmax&lt;/code&gt;: The Transaction ID of the transaction that deleted or updated this row version (0 if the row is still live and un-deleted).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t_ctid&lt;/code&gt;: The physical location &lt;code&gt;(page_number, line_pointer_index)&lt;/code&gt; of this tuple or the newer version tuple if updated.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t_infomask&lt;/code&gt; / &lt;code&gt;t_infomask2&lt;/code&gt;: Bit flags indicating transaction commit status and Heap-Only Tuple properties.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. The Step-by-Step Trace of an UPDATE
&lt;/h2&gt;

&lt;p&gt;Let us trace what happens under the hood when Transaction 105 updates row 1 on page 0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial State: A Fresh Insert (Transaction 100)
&lt;/h3&gt;

&lt;p&gt;When row 1 is originally inserted by Transaction 100 at page 0, line pointer 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Physical Address: ctid = (0, 1)
xmin: 100
xmax: 0
data: { id: 42, name: 'Alice', status: 'pending' }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any transaction running concurrently checks &lt;code&gt;xmin&lt;/code&gt; (100 is committed) and &lt;code&gt;xmax&lt;/code&gt; (0 means not deleted), so it reads the row.&lt;/p&gt;

&lt;h3&gt;
  
  
  The UPDATE Executes (Transaction 105)
&lt;/h3&gt;

&lt;p&gt;When Transaction 105 executes &lt;code&gt;UPDATE users SET status = 'active' WHERE id = 42&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: Write New Tuple (Physical Address: ctid (0, 2))
------------------------------------------------------
xmin: 105
xmax: 0
data: { id: 42, name: 'Alice', status: 'active' }

Step 2: Expire Old Tuple (Physical Address: ctid (0, 1))
------------------------------------------------------
xmin: 100
xmax: 105               &amp;lt;-- Marked deleted by Tx 105!
t_ctid: (0, 2)          &amp;lt;-- Points forward to the replacement version!
data: { id: 42, name: 'Alice', status: 'pending' }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
| PAGE 0                                                                |
|                                                                       |
| Line Pointer 1 ---------&amp;gt; [Tuple 1: xmin=100, xmax=105, ctid=(0,2)]   |
|                                   | (Forward Pointer)                 |
|                                   v                                   |
| Line Pointer 2 ---------&amp;gt; [Tuple 2: xmin=105, xmax=0,   ctid=(0,2)]   |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what just happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tuple 1 was &lt;strong&gt;not&lt;/strong&gt; overwritten. Its &lt;code&gt;xmax&lt;/code&gt; was updated to 105, and its &lt;code&gt;t_ctid&lt;/code&gt; was updated to point to &lt;code&gt;(0, 2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tuple 2 was written as a fresh row at line pointer 2 with &lt;code&gt;xmin = 105&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both tuples now exist simultaneously on the physical disk page.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. How MVCC Solves Concurrency Without Read Locks
&lt;/h2&gt;

&lt;p&gt;Why go through this trouble instead of modifying bytes in place?&lt;/p&gt;

&lt;p&gt;Because readers never block writers, and writers never block readers.&lt;/p&gt;

&lt;p&gt;Consider two concurrent transactions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transaction 102 (Started earlier, long-running report):&lt;/strong&gt;&lt;br&gt;
When Tx 102 queries &lt;code&gt;users WHERE id = 42&lt;/code&gt;, its snapshot rule says: &lt;em&gt;"Ignore changes from any transaction ID &amp;gt;= 102."&lt;/em&gt;&lt;br&gt;
It inspects Tuple 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xmin&lt;/code&gt; is 100 (committed before 102: visible).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xmax&lt;/code&gt; is 105 (committed after 102 started: invisible deletion).
Tx 102 reads Tuple 1 (&lt;code&gt;status: 'pending'&lt;/code&gt;) cleanly without taking a single shared read lock on the row.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transaction 108 (Started after 105 commits):&lt;/strong&gt;&lt;br&gt;
Its snapshot rule says: &lt;em&gt;"Transaction 105 is committed."&lt;/em&gt;&lt;br&gt;
It inspects Tuple 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xmax&lt;/code&gt; is 105 (committed: Tuple 1 is dead).
It follows the forward pointer &lt;code&gt;t_ctid = (0, 2)&lt;/code&gt; to Tuple 2:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xmin&lt;/code&gt; is 105 (committed: visible).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xmax&lt;/code&gt; is 0 (live).
Tx 108 reads Tuple 2 (&lt;code&gt;status: 'active'&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No locks, no mutexes, no dirty reads, and no rollbacks. If Transaction 105 aborts, Postgres simply marks Tx 105 as aborted in the &lt;code&gt;pg_xact&lt;/code&gt; commit log status map. Tuple 2 becomes instantly invisible to everyone, and Tuple 1 becomes visible again. Zero rollback log application needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Hidden Tax: The Secondary Index Disaster
&lt;/h2&gt;

&lt;p&gt;The MVCC design is brilliant for concurrency, but it introduces a severe architectural problem with database indexes.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, secondary indexes (B-Trees, GIN, GiST) do not point to a primary key or clustered table structure (unlike MySQL InnoDB). In Postgres, &lt;strong&gt;all indexes store direct physical pointers (&lt;code&gt;ctid&lt;/code&gt;) to heap tuples&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;users&lt;/code&gt; table has 5 indexes (e.g. on &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;org_id&lt;/code&gt;, &lt;code&gt;last_login&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before UPDATE:
Index on email       -------&amp;gt; ctid (0, 1)
Index on created_at  -------&amp;gt; ctid (0, 1)
Index on org_id      -------&amp;gt; ctid (0, 1)
Index on last_login  -------&amp;gt; ctid (0, 1)
Index on status      -------&amp;gt; ctid (0, 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you update just one column (&lt;code&gt;status&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The new row is written to &lt;code&gt;ctid (0, 2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If naive indexing applied, Postgres would have to insert a new index entry pointing to &lt;code&gt;(0, 2)&lt;/code&gt; into &lt;strong&gt;all 5 indexes&lt;/strong&gt;, even though &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;org_id&lt;/code&gt;, and &lt;code&gt;last_login&lt;/code&gt; never changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a write-heavy database with 10 million rows and multiple indexes, updating a single column would trigger massive B-Tree write amplification, leaf page splits, and catastrophic disk I/O.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Solution: Heap-Only Tuples (HOT)
&lt;/h2&gt;

&lt;p&gt;To fix this index amplification, Postgres 8.3 introduced &lt;strong&gt;HOT (Heap-Only Tuples)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;HOT allows Postgres to update a row without touching secondary indexes at all, provided two conditions are met:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No indexed column is modified:&lt;/strong&gt; The &lt;code&gt;UPDATE&lt;/code&gt; statement only changes columns that are not part of any index definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Space exists on the same page:&lt;/strong&gt; The new tuple version fits inside the exact same 8KB physical page as the old tuple.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index on email ------&amp;gt; Line Pointer 1 (LP_REDIRECT)
                             |
                             v
                       Line Pointer 2 ------&amp;gt; [Tuple 2 (status: 'active')]
                                              [Tuple 1 (Dead: status: 'pending')]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When HOT activates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Postgres writes Tuple 2 to the same page.&lt;/li&gt;
&lt;li&gt;Postgres turns Line Pointer 1 into an &lt;code&gt;LP_REDIRECT&lt;/code&gt; stub pointing directly to Line Pointer 2 inside the page header.&lt;/li&gt;
&lt;li&gt;Every secondary index continues pointing to Line Pointer 1 without any index modifications.&lt;/li&gt;
&lt;li&gt;When an index scan traverses the B-Tree to Line Pointer 1, the engine follows the internal line pointer redirection directly to Tuple 2.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if your table is benefiting from HOT updates&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;n_tup_upd&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;n_tup_hot_upd&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hot_updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n_tup_hot_upd&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_tup_upd&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hot_ratio_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your &lt;code&gt;hot_ratio_pct&lt;/code&gt; is low (below 80% on update-heavy tables), you are paying a massive write penalty.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Inspecting the Physical Page with &lt;code&gt;pageinspect&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You can verify this physical behavior directly using the built-in Postgres &lt;code&gt;pageinspect&lt;/code&gt; extension.&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;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pageinspect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Inspect line pointers and tuple headers on block 0 of users table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;lp_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;t_xmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;t_xmax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;t_ctid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;t_infomask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;t_data&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;heap_page_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_raw_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this after an &lt;code&gt;UPDATE&lt;/code&gt;, you will literally see two separate items on the page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Item &lt;code&gt;lp=1&lt;/code&gt; with &lt;code&gt;t_xmax = &amp;lt;txid&amp;gt;&lt;/code&gt; and &lt;code&gt;t_ctid = (0, 2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Item &lt;code&gt;lp=2&lt;/code&gt; with &lt;code&gt;t_xmin = &amp;lt;txid&amp;gt;&lt;/code&gt; and &lt;code&gt;t_xmax = 0&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Table Bloat and the Autovacuum Lifecycle
&lt;/h2&gt;

&lt;p&gt;Because updates leave dead tuples behind, a table that processes 100,000 updates a day accumulates 100,000 dead rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Role of VACUUM
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;VACUUM&lt;/code&gt; runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It scans pages and identifies tuples whose &lt;code&gt;xmax&lt;/code&gt; is older than the oldest active transaction snapshot.&lt;/li&gt;
&lt;li&gt;It removes the dead tuple payload and marks the space on the 8KB page as available for future &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt; operations.&lt;/li&gt;
&lt;li&gt;It updates the Free Space Map (&lt;code&gt;_fsm&lt;/code&gt; fork) so new writes know where to find free slots.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Bloat Trap: VACUUM Does Not Shrink File Size
&lt;/h3&gt;

&lt;p&gt;Standard &lt;code&gt;VACUUM&lt;/code&gt; frees space &lt;em&gt;inside&lt;/em&gt; existing pages, but it &lt;strong&gt;never returns pages to the operating system filesystem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a table bloats from 1 GB to 20 GB during a bulk update batch, the disk file stays 20 GB forever unless:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Future inserts fill the empty slots inside the existing pages.&lt;/li&gt;
&lt;li&gt;You run &lt;code&gt;VACUUM FULL users&lt;/code&gt; or &lt;code&gt;pg_repack&lt;/code&gt;, which creates a brand new copy of the table and swaps the physical relfilenode on disk.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------------------------------------------------------------+
| Disk File (20 GB on OS)                                       |
| [Page 0: 80% free] [Page 1: 90% free] ... [Page 2,500,000]    |
+---------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Senior Database Gotchas to Prevent MVCC Outages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Long-Running Transaction Anchor
&lt;/h3&gt;

&lt;p&gt;If a developer opens a &lt;code&gt;BEGIN;&lt;/code&gt; session in &lt;code&gt;psql&lt;/code&gt; or an analytics query runs for 4 hours, Postgres cannot vacuum any dead tuples created after that transaction started.&lt;br&gt;
Autovacuum will skip dead tuples, leading to exponential table bloat and high disk consumption.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find transactions holding back VACUUM cleanup&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;usename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backend_xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;xmin_age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;xact_start&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;backend_xmin&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backend_xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Tuning Table &lt;code&gt;fillfactor&lt;/code&gt; for HOT Updates
&lt;/h3&gt;

&lt;p&gt;By default, Postgres packs 100% data into every 8KB page (&lt;code&gt;fillfactor = 100&lt;/code&gt;). This leaves zero empty room for HOT updates on the same page, forcing every update to allocate a new page and update all secondary indexes.&lt;/p&gt;

&lt;p&gt;For write-heavy tables with frequent updates, set &lt;code&gt;fillfactor&lt;/code&gt; to 80-90%:&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;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fillfactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="k"&gt;FULL&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Rewrites the table with 20% free space per page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, 20% of every page is reserved specifically for in-page HOT updates, eliminating secondary index churn.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Autovacuum Cost Delay Throttling
&lt;/h3&gt;

&lt;p&gt;Default PostgreSQL autovacuum settings are deliberately conservative (&lt;code&gt;autovacuum_vacuum_cost_limit = 200&lt;/code&gt;). Under write load, dead tuples accumulate 10x faster than autovacuum can process them.&lt;br&gt;
Tune &lt;code&gt;autovacuum_vacuum_cost_limit&lt;/code&gt; to &lt;code&gt;1000&lt;/code&gt; or &lt;code&gt;2000&lt;/code&gt; on modern NVMe SSDs to allow vacuum workers to keep up with write streams.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Mental Model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;What Developers Assume&lt;/th&gt;
&lt;th&gt;What Postgres Actually Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Modifies bytes in place on disk&lt;/td&gt;
&lt;td&gt;Writes a new row (&lt;code&gt;xmin&lt;/code&gt;), marks old row dead (&lt;code&gt;xmax&lt;/code&gt;), updates &lt;code&gt;t_ctid&lt;/code&gt; pointer chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Erases bytes from disk&lt;/td&gt;
&lt;td&gt;Sets &lt;code&gt;xmax = current_txid&lt;/code&gt; in the tuple header (soft delete)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read locks prevent dirty reads&lt;/td&gt;
&lt;td&gt;Snapshot visibility check on &lt;code&gt;xmin&lt;/code&gt;/&lt;code&gt;xmax&lt;/code&gt; headers without read locks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Index Impact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No index changes if keys are untouched&lt;/td&gt;
&lt;td&gt;All indexes get new entries unless HOT (Heap-Only Tuples) optimization triggers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;VACUUM&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shrinks database file on disk&lt;/td&gt;
&lt;td&gt;Reclaims internal page space for future writes; does not shrink OS file size&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Understanding that an &lt;code&gt;UPDATE&lt;/code&gt; is an &lt;code&gt;INSERT&lt;/code&gt; changes how you design schema indexes, tune &lt;code&gt;fillfactor&lt;/code&gt;, configure autovacuum, and manage transaction lifetimes in production.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>performance</category>
      <category>systems</category>
    </item>
    <item>
      <title>How Linux Zero-Copy Actually Works: sendfile, DMA Scatter-Gather, and Page Cache Internals</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:33:31 +0000</pubDate>
      <link>https://dev.to/syed_anzar/how-linux-zero-copy-actually-works-sendfile-dma-scatter-gather-and-page-cache-internals-4g0o</link>
      <guid>https://dev.to/syed_anzar/how-linux-zero-copy-actually-works-sendfile-dma-scatter-gather-and-page-cache-internals-4g0o</guid>
      <description>&lt;p&gt;When building high-throughput web servers, reverse proxies, or message brokers like Kafka and NGINX, you inevitably run into the term &lt;strong&gt;Zero-Copy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most developers understand the high-level pitch: &lt;em&gt;Zero-copy transfers data from disk to network without wasting CPU cycles copying bytes into user space.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While that summary is accurate, it skips the actual engineering underneath:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where do the copies actually happen in a traditional &lt;code&gt;read()&lt;/code&gt; and &lt;code&gt;write()&lt;/code&gt; loop?&lt;/li&gt;
&lt;li&gt;How does the kernel send data it never copied into memory?&lt;/li&gt;
&lt;li&gt;Why did HTTPS break zero-copy for nearly a decade, and how did the Linux kernel fix it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's trace what happens inside the Linux kernel, CPU registers, page cache, and network interface card (NIC) when you move bytes with and without zero-copy.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Hidden Tax of Traditional I/O (&lt;code&gt;read&lt;/code&gt; + &lt;code&gt;write&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Consider the standard way a beginner writes a static file server in C, Python, or Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes_read&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;To transfer one chunk of data, the OS goes through 4 context switches and 4 data copies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
| USER SPACE                                                            |
|                           [ User Buffer ]                             |
|                             ^         |                               |
|                     CPU Copy|         |CPU Copy                       |
|                             |         v                               |
+-----------------------------|---------|-------------------------------+
| KERNEL SPACE                |         |                               |
|                     +-------+         +-------+                       |
|                     |                         |                       |
|                     v                         v                       |
|              [ Page Cache ]             [ Socket Buffer ]             |
|                     ^                         |                       |
|              DMA Copy|                         |DMA Copy               |
|                     |                         v                       |
|                  [ Disk ]                   [ NIC ]                   |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Step-by-Step Breakdown
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;read()&lt;/code&gt; Syscall (User -&amp;gt; Kernel Switch 1):&lt;/strong&gt; The CPU traps into kernel mode. The kernel issues a DMA (Direct Memory Access) request to read the file from storage into kernel page cache memory (&lt;strong&gt;Copy 1: Disk -&amp;gt; Page Cache&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel -&amp;gt; User Copy (Switch 2):&lt;/strong&gt; The CPU copies the raw bytes from the kernel page cache into the process's stack or heap buffer (&lt;strong&gt;Copy 2: Page Cache -&amp;gt; User Buffer&lt;/strong&gt;). The syscall returns to userspace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;write()&lt;/code&gt; Syscall (User -&amp;gt; Kernel Switch 3):&lt;/strong&gt; The process calls &lt;code&gt;write()&lt;/code&gt;, trapping back into kernel mode. The CPU copies data from the user buffer into the kernel's socket send buffer (&lt;strong&gt;Copy 3: User Buffer -&amp;gt; Socket Buffer&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Socket -&amp;gt; NIC DMA (Switch 4):&lt;/strong&gt; The kernel returns from &lt;code&gt;write()&lt;/code&gt;. Asynchronously, the NIC engine reads the socket buffer via DMA and sends the packets across the physical wire (&lt;strong&gt;Copy 4: Socket Buffer -&amp;gt; NIC&lt;/strong&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Is This Expensive?
&lt;/h3&gt;

&lt;p&gt;The DMA copies (Disk to RAM, RAM to NIC) are handled by hardware controllers without consuming CPU cycles.&lt;/p&gt;

&lt;p&gt;The real bottleneck is the &lt;strong&gt;2 intermediate CPU copies&lt;/strong&gt; across the kernel-userspace boundary. When serving gigabytes of data per second:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CPU spends billions of clock cycles copying memory blocks (&lt;code&gt;memcpy&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;It saturates the system memory bus.&lt;/li&gt;
&lt;li&gt;It evicts valuable hot data from the CPU L1, L2, and L3 caches to make room for transient network payloads.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. Evolution Level 1: &lt;code&gt;mmap()&lt;/code&gt; + &lt;code&gt;write()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first optimization historically attempted was memory-mapped I/O via &lt;code&gt;mmap()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROT_READ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAP_SHARED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_fd&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="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By mapping the kernel's page cache directly into the process's virtual address space, &lt;code&gt;mmap()&lt;/code&gt; eliminates the copy into the user buffer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disk -&amp;gt; Page Cache (DMA)&lt;/li&gt;
&lt;li&gt;Page Cache -&amp;gt; Socket Buffer (CPU Copy)&lt;/li&gt;
&lt;li&gt;Socket Buffer -&amp;gt; NIC (DMA)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This cuts down CPU copies from 2 to 1. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Hidden Trap: &lt;code&gt;SIGBUS&lt;/code&gt; Crashes
&lt;/h3&gt;

&lt;p&gt;If another process truncates or deletes the file while your thread is executing &lt;code&gt;write(socket_fd, file_ptr, ...)&lt;/code&gt;, accessing that memory address generates a &lt;code&gt;SIGBUS&lt;/code&gt; (Bus Error) signal. Unless your application installs a custom signal handler, the process crashes instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Evolution Level 2: &lt;code&gt;sendfile()&lt;/code&gt; Without Hardware Support
&lt;/h2&gt;

&lt;p&gt;Linux 2.2 introduced the &lt;code&gt;sendfile()&lt;/code&gt; syscall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/sendfile.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="nf"&gt;sendfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;out_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;in_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;off_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sendfile()&lt;/code&gt; instructs the kernel to transfer &lt;code&gt;count&lt;/code&gt; bytes directly from &lt;code&gt;in_fd&lt;/code&gt; (a file) to &lt;code&gt;out_fd&lt;/code&gt; (a network socket).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
| USER SPACE                                                            |
|                      (No data enters userspace)                       |
+-----------------------------------------------------------------------+
| KERNEL SPACE                                                          |
|              [ Page Cache ] ---- CPU Copy ----&amp;gt; [ Socket Buffer ]     |
|                     ^                                  |              |
|              DMA Copy|                                  |DMA Copy      |
|                     |                                  v              |
|                  [ Disk ]                            [ NIC ]          |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context switches dropped from 4 to 2 (only one syscall invocation).&lt;/li&gt;
&lt;li&gt;Zero user-space memory allocations.&lt;/li&gt;
&lt;li&gt;But on older kernels or basic hardware, there is still &lt;strong&gt;1 CPU copy&lt;/strong&gt; between the page cache and the socket buffer.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Evolution Level 3: True Zero-Copy (Scatter-Gather DMA)
&lt;/h2&gt;

&lt;p&gt;Starting in Linux 2.4, the kernel introduced &lt;strong&gt;Scatter-Gather DMA&lt;/strong&gt; support for network sockets (&lt;code&gt;NETIF_F_SG&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When the network card supports scatter-gather, the CPU no longer copies file data into the socket buffer. Instead, the kernel only passes &lt;strong&gt;buffer descriptors (pointers and lengths)&lt;/strong&gt; to the socket buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
| USER SPACE                                                            |
|                      (No data enters userspace)                       |
+-----------------------------------------------------------------------+
| KERNEL SPACE                                                          |
|              [ Page Cache (Physical RAM) ]                            |
|                     ^           |                                     |
|              DMA Copy           | Direct DMA Read                     |
|              from Disk          |                                     |
|                     |           v                                     |
|                  [ Disk ]    [ NIC ] &amp;lt;--- Packet Headers              |
|                                 ^                                     |
|                                 | DMA Read                            |
|                       [ Socket Buffer ]                               |
|                 (File Descriptors &amp;amp; Headers Only)                     |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How &lt;code&gt;struct sk_buff&lt;/code&gt; Handles This Internally
&lt;/h3&gt;

&lt;p&gt;Inside the kernel networking stack, every packet is represented by a &lt;code&gt;struct sk_buff&lt;/code&gt; (socket buffer).&lt;/p&gt;

&lt;p&gt;Instead of allocating contiguous memory to hold both the TCP/IP headers and the file payload, the kernel uses fragmented skb buffers (&lt;code&gt;skb_shinfo&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;skb_shared_info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
    &lt;span class="n"&gt;nr_frags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skb_frag_t&lt;/span&gt; &lt;span class="n"&gt;frags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MAX_SKB_FRAGS&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;Each &lt;code&gt;skb_frag_t&lt;/code&gt; holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pointer to the physical memory page (&lt;code&gt;struct page *&lt;/code&gt;) in the Page Cache.&lt;/li&gt;
&lt;li&gt;An offset inside that page.&lt;/li&gt;
&lt;li&gt;The byte length.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the packet is scheduled for transmission:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The kernel constructs the TCP/IP headers in memory.&lt;/li&gt;
&lt;li&gt;The NIC's DMA engine receives a scatter-gather list:

&lt;ul&gt;
&lt;li&gt;Descriptor 1: Read 54 bytes (TCP/IP headers) from kernel memory.&lt;/li&gt;
&lt;li&gt;Descriptor 2: Read 1460 bytes (file payload) directly from the page cache address.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The NIC controller gathers the memory regions over the PCIe bus and stitches them into a single Ethernet frame on the wire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;CPU copies performed: Exactly ZERO.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. &lt;code&gt;splice()&lt;/code&gt;: Zero-Copy Between Arbitrary File Descriptors
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;sendfile()&lt;/code&gt; is designed specifically for streaming files to sockets, Linux 2.6.17 introduced &lt;code&gt;splice()&lt;/code&gt; to support zero-copy streaming between arbitrary file descriptors (pipes, sockets, block devices):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define _GNU_SOURCE
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;fcntl.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loff_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;off_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loff_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;off_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
               &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Magic of Pipe Buffers (&lt;code&gt;struct pipe_buffer&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;splice()&lt;/code&gt; works using kernel pipe buffers (&lt;code&gt;struct pipe_inode_info&lt;/code&gt;). A pipe buffer in Linux does not allocate memory for data bytes; it is a circular ring of page pointers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pipe_buffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pipe_buf_operations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&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;When you splice from a socket into a pipe, the kernel pins the network incoming page into the ring buffer. When you splice from that pipe into a file or another socket, the kernel transfers ownership of those page pointers without copying a single byte.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Modern HTTPS Problem: Why TLS Broke Zero-Copy
&lt;/h2&gt;

&lt;p&gt;For years, zero-copy had a massive limitation: &lt;strong&gt;Encryption&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your web server runs HTTPS, traditional OpenSSL or BoringSSL pipelines look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read encrypted/raw data into userspace.&lt;/li&gt;
&lt;li&gt;Run AES-GCM encryption in user memory.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;write()&lt;/code&gt; with the ciphertext.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because encryption requires reading and modifying every single byte, &lt;code&gt;sendfile()&lt;/code&gt; was useless for HTTPS. Web servers were forced back into the slow 4-switch, 2-copy path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: kTLS (Kernel TLS)
&lt;/h3&gt;

&lt;p&gt;Linux 4.13 introduced &lt;strong&gt;kTLS&lt;/strong&gt; (Kernel TLS, via &lt;code&gt;SO_TLS_TX&lt;/code&gt; / &lt;code&gt;TCP_ULP&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Enable Kernel TLS User-Level Protocol on the TCP socket */&lt;/span&gt;
&lt;span class="n"&gt;setsockopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SOL_TCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TCP_ULP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tls"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="cm"&gt;/* Provide the symmetric cipher keys (AES-GCM-128 / 256) negotiated in userspace */&lt;/span&gt;
&lt;span class="n"&gt;setsockopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SOL_TLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TLS_TX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;crypto_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto_info&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once kTLS is enabled:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The userspace library (such as OpenSSL or Rustls) completes the initial TLS 1.3 handshake.&lt;/li&gt;
&lt;li&gt;The session encryption keys are handed off to the kernel socket options.&lt;/li&gt;
&lt;li&gt;The application calls standard &lt;code&gt;sendfile()&lt;/code&gt;!&lt;/li&gt;
&lt;li&gt;The kernel encrypts the page cache buffers inline as they flow through the network stack, or offloads encryption entirely to SmartNICs with TLS hardware accelerators.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This restored zero-copy throughput for HTTPS static file delivery in modern NGINX, HAProxy, and Envoy deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Zero-Copy Performance Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Syscalls Used&lt;/th&gt;
&lt;th&gt;Context Switches&lt;/th&gt;
&lt;th&gt;CPU Memory Copies&lt;/th&gt;
&lt;th&gt;DMA Copies&lt;/th&gt;
&lt;th&gt;TLS Compatible?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traditional I/O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;read()&lt;/code&gt; + &lt;code&gt;write()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Yes (Userspace crypto)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Mapped&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mmap()&lt;/code&gt; + &lt;code&gt;write()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Yes (Userspace crypto)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;sendfile()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sendfile()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1 (0 with SG-DMA)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Only with kTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;splice()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;splice()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Only with kTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;MSG_ZEROCOPY&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;send(..., MSG_ZEROCOPY)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0 (Page pinning)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;App-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  8. Senior Engineer Gotchas to Keep in Mind
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MSG_ZEROCOPY&lt;/code&gt; on small payloads is a trap:&lt;/strong&gt; For payloads under 10-16 KB, standard &lt;code&gt;send()&lt;/code&gt; is faster. &lt;code&gt;MSG_ZEROCOPY&lt;/code&gt; on raw memory buffers requires the kernel to pin user pages (&lt;code&gt;get_user_pages_fast&lt;/code&gt;), set up completion notifications on the socket error queue (&lt;code&gt;sock_extended_err&lt;/code&gt;), and notify userspace via &lt;code&gt;poll()&lt;/code&gt;/&lt;code&gt;epoll()&lt;/code&gt;. The bookkeeping overhead easily exceeds the cost of a small &lt;code&gt;memcpy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File modification during transfer:&lt;/strong&gt; If you modify a file in place while &lt;code&gt;sendfile()&lt;/code&gt; is actively reading page cache pages, the client will receive corrupted, mixed data frames. Always use atomic file replacements (rename/symlinks) for live assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dirty Page Flushing:&lt;/strong&gt; Zero-copy does not bypass Linux caching rules. Data must exist in the kernel page cache; if pages are evicted under memory pressure, synchronous disk reads will stall the I/O thread.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Zero-copy is not about avoiding disk or network hardware transfers; it is about &lt;strong&gt;eliminating redundant CPU memory copies and context switches across the kernel-userspace boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By passing page descriptors (&lt;code&gt;struct page *&lt;/code&gt;) through &lt;code&gt;struct sk_buff&lt;/code&gt; and leveraging NIC Scatter-Gather DMA, the Linux kernel lets hardware controllers stream data straight from disk cache to the wire while keeping your CPU caches clean and cool.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systems</category>
      <category>performance</category>
      <category>networking</category>
    </item>
    <item>
      <title>How Model Context Protocol (MCP) Actually Works: JSON-RPC 2.0, Stdio IPC, and Low-Level Tool Calling</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:16:58 +0000</pubDate>
      <link>https://dev.to/syed_anzar/how-model-context-protocol-mcp-actually-works-json-rpc-20-stdio-ipc-and-low-level-tool-calling-2b41</link>
      <guid>https://dev.to/syed_anzar/how-model-context-protocol-mcp-actually-works-json-rpc-20-stdio-ipc-and-low-level-tool-calling-2b41</guid>
      <description>&lt;h1&gt;
  
  
  How Model Context Protocol (MCP) Actually Works: JSON-RPC 2.0, Stdio IPC, and Low-Level Tool Calling
&lt;/h1&gt;

&lt;p&gt;If you have used Claude Desktop, Cursor, or modern local coding agents recently, you have probably configured a &lt;code&gt;claude_desktop_config.json&lt;/code&gt; file or launched an MCP server with &lt;code&gt;npx -y @modelcontextprotocol/server-postgres&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the surface, it looks almost magical: an LLM suddenly connects to your local SQLite database, reads your local files, and executes commands.&lt;/p&gt;

&lt;p&gt;Most tutorials explain MCP like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"MCP is an open standard that lets AI models interact with tools and data sources."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That definition tells you what it does, not how it actually works under the hood.&lt;/p&gt;

&lt;p&gt;What actually happens between the moment you type a prompt and the moment a local script executes on your machine? How does a stateless LLM communicate with a stateful local process? Why does standard output (&lt;code&gt;stdout&lt;/code&gt;) have a strict protocol rule that breaks your server if you use &lt;code&gt;print()&lt;/code&gt; or &lt;code&gt;console.log()&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Let us trace the entire lifecycle: from file descriptor pipes and JSON-RPC 2.0 frames to capability negotiation and token overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Architectural Problem MCP Actually Solved
&lt;/h2&gt;

&lt;p&gt;Before Anthropic open-sourced the Model Context Protocol in late 2024, the AI ecosystem was drowning in the &lt;strong&gt;M × N Integration Problem&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE MCP (M × N Custom Integrations):
[Claude]    ---&amp;gt; Custom Plugin API ---&amp;gt; [Postgres]
[ChatGPT]   ---&amp;gt; Actions Schema    ---&amp;gt; [GitHub]
[Cursor]    ---&amp;gt; Custom Extension  ---&amp;gt; [Slack]
[LangChain] ---&amp;gt; BaseTool Wrapper  ---&amp;gt; [Local Filesystem]

WITH MCP (1 Standard Protocol):
[Any Host / Agent]  &amp;lt;=== JSON-RPC 2.0 ===&amp;gt;  [Any MCP Server]
(Claude, Cursor,                           (DBs, APIs, Files,
 Hermes, LangChain)                         Git, Local CLI)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you had 5 agent hosts and 20 tools, developers had to write and maintain 100 separate integration wrappers.&lt;/p&gt;

&lt;p&gt;MCP borrowed the architectural playbook of Microsoft's &lt;strong&gt;Language Server Protocol (LSP)&lt;/strong&gt;. Instead of every code editor writing a custom parser for TypeScript, Python, and Rust, LSP created a single standard JSON-RPC protocol. &lt;/p&gt;

&lt;p&gt;MCP is LSP for AI agents.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Three Core Entities
&lt;/h2&gt;

&lt;p&gt;An MCP architecture consists of three distinct participants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────┐
│ HOST APPLICATION (e.g., Claude Desktop, Cursor, Hermes)   │
│                                                          │
│  ┌───────────────────────┐      ┌─────────────────────┐  │
│  │ User Interface / UX   │      │ LLM Inference Engine│  │
│  └───────────┬───────────┘      └──────────┬──────────┘  │
│              │                             │             │
│              ▼                             ▼             │
│  ┌────────────────────────────────────────────────────┐  │
│  │ MCP CLIENT (Protocol Controller &amp;amp; State Manager)   │  │
│  └──────────┬──────────────────────────────┬──────────┘  │
└─────────────┼──────────────────────────────┼─────────────┘
              │ (Stdio Pipe / FD 0 &amp;amp; 1)      │ (HTTP + SSE)
              ▼                              ▼
┌──────────────────────────┐   ┌──────────────────────────┐
│ LOCAL MCP SERVER         │   │ REMOTE MCP SERVER        │
│ (sqlite, git, filesystem)│   │ (github, slack, db api)  │
└──────────────────────────┘   └──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Host&lt;/strong&gt;: The user-facing application (Claude Desktop, Cursor, Hermes CLI). The host controls permissions, displays UI, and handles LLM model interactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt;: The internal protocol controller inside the host. A client maintains a strict 1:1 connection with each MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt;: An isolated program (local CLI or remote service) that exposes tools, data resources, and prompt templates through the protocol.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. The Wire Protocol: JSON-RPC 2.0
&lt;/h2&gt;

&lt;p&gt;Underneath the high-level SDKs (&lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; in TypeScript or &lt;code&gt;mcp&lt;/code&gt; in Python), MCP communicates exclusively via &lt;strong&gt;JSON-RPC 2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every message transmitted over the wire falls into one of three structural types:&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Request (Client to Server or Server to Client)
&lt;/h3&gt;

&lt;p&gt;A request requires an explicit response. It carries a unique &lt;code&gt;id&lt;/code&gt; to correlate asynchronous replies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"query_database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT id, email FROM users WHERE status = 'active' LIMIT 5;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  B. Response (Success or Error)
&lt;/h3&gt;

&lt;p&gt;The receiver matches the incoming &lt;code&gt;id&lt;/code&gt; and returns either a &lt;code&gt;result&lt;/code&gt; payload or an &lt;code&gt;error&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 1, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;alex@example.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}]"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an execution failure occurs, the server returns standard JSON-RPC error codes (such as &lt;code&gt;-32600&lt;/code&gt; for Invalid Request or &lt;code&gt;-32601&lt;/code&gt; for Method Not Found):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32602&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid params: 'query' field cannot be empty"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C. Notification (One-Way Signaling)
&lt;/h3&gt;

&lt;p&gt;Notifications never contain an &lt;code&gt;id&lt;/code&gt; field and must not be answered. They are used for fire-and-forget events like logging or state updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notifications/tools/list_changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Stdio Transport: File Descriptors and the Stderr Rule
&lt;/h2&gt;

&lt;p&gt;When running an MCP server locally, the host starts the server as a child sub-process using standard Operating System process pipes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HOST (Client Process)                      CHILD (MCP Server Process)
┌──────────────────────┐                   ┌────────────────────────┐
│                      │ ─── Stdin (FD 0) ──&amp;gt;│ JSON Parser Reader   │
│ Client Runtime       │                   │                        │
│                      │ &amp;lt;── Stdout (FD 1) ──│ JSON-RPC Serializer  │
│                      │                   │                        │
│ Log Collector / UI   │ &amp;lt;── Stderr (FD 2) ──│ Debug / Diagnostic   │
└──────────────────────┘                   └────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This transport mechanism has critical engineering implications:&lt;/p&gt;

&lt;h3&gt;
  
  
  File Descriptor Roles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard Input (FD 0)&lt;/strong&gt;: The host writes newline-delimited (&lt;code&gt;\n&lt;/code&gt;) JSON-RPC messages directly to the server's input stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard Output (FD 1)&lt;/strong&gt;: The server writes its newline-delimited JSON-RPC responses to stdout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard Error (FD 2)&lt;/strong&gt;: Reserved strictly for human-readable logging and diagnostics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Most Common MCP Server Bug
&lt;/h3&gt;

&lt;p&gt;If you write an MCP server in Python or Node.js and include a diagnostic &lt;code&gt;print("Connected to DB")&lt;/code&gt; or &lt;code&gt;console.log("Fetching data...")&lt;/code&gt;, your server will immediately crash.&lt;/p&gt;

&lt;p&gt;Why? Standard &lt;code&gt;print()&lt;/code&gt; in Python writes directly to &lt;strong&gt;stdout (FD 1)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When the host client's stream parser reads a raw string like &lt;code&gt;"Connected to DB"&lt;/code&gt; instead of a valid JSON object, the JSON parser fails with a syntax error, drops the connection, and kills the session.&lt;/p&gt;

&lt;p&gt;To log safely in an MCP server, you must explicitly redirect output to &lt;strong&gt;stderr (FD 2)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="c1"&gt;# CRASHES THE MCP CLIENT (Pollutes FD 1):
# print("Processing record...")
&lt;/span&gt;
&lt;span class="c1"&gt;# SAFE: Directs log message to FD 2:
&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DEBUG] Processing record...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. SSE Transport: Remote Execution Over HTTP
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;stdio&lt;/code&gt; is ideal for local desktop environments, distributed microservices require network transport. MCP handles this via &lt;strong&gt;Server-Sent Events (SSE) over HTTP&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLIENT (Host)                                 SERVER (Remote Endpoint)
   │                                                    │
   │ 1. GET /sse (Accept: text/event-stream)            │
   │───────────────────────────────────────────────────&amp;gt;│
   │                                                    │
   │ 2. HTTP 200 SSE Stream Opened                     │
   │    event: endpoint                                 │
   │    data: /messages?sessionId=a8f9-4b21             │
   │&amp;lt;───────────────────────────────────────────────────│
   │                                                    │
   │ 3. POST /messages?sessionId=a8f9-4b21 (JSON-RPC)   │
   │───────────────────────────────────────────────────&amp;gt;│
   │    HTTP 202 Accepted                               │
   │&amp;lt;───────────────────────────────────────────────────│
   │                                                    │
   │ 4. SSE Push (JSON-RPC Response)                    │
   │    event: message                                  │
   │    data: {"jsonrpc":"2.0","id":1,"result":{...}}   │
   │&amp;lt;───────────────────────────────────────────────────│
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why did MCP choose SSE + HTTP POST instead of WebSockets?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Firewall &amp;amp; Proxy Compatibility&lt;/strong&gt;: SSE operates over plain HTTP/1.1 or HTTP/2 streams without requiring WebSocket upgrade handshakes that often get blocked in enterprise proxies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Reconnection&lt;/strong&gt;: Browsers and HTTP clients have native SSE reconnection semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of Upstream &amp;amp; Downstream&lt;/strong&gt;: Heavy payloads sent via POST do not choke the incoming event stream.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. The Protocol Lifecycle: Handshake to Execution
&lt;/h2&gt;

&lt;p&gt;Before an LLM can invoke a single function, the client and server complete a 3-step initialization handshake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLIENT                                                   SERVER
  │                                                        │
  │ 1. Request: "initialize"                               │
  │    (Protocol Version + Client Capabilities)            │
  │───────────────────────────────────────────────────────&amp;gt;│
  │                                                        │
  │ 2. Response: Server Info &amp;amp; Capabilities                │
  │    (tools, resources, prompts, logging)                │
  │&amp;lt;───────────────────────────────────────────────────────│
  │                                                        │
  │ 3. Notification: "notifications/initialized"           │
  │───────────────────────────────────────────────────────&amp;gt;│
  │                                                        │
  │ 4. Request: "tools/list"                               │
  │───────────────────────────────────────────────────────&amp;gt;│
  │                                                        │
  │ 5. Response: Array of Tool Definitions (JSON Schema)   │
  │&amp;lt;───────────────────────────────────────────────────────│
  │                                                        │
  │           [Session is now Ready for Tool Calls]        │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: The &lt;code&gt;initialize&lt;/code&gt; Request
&lt;/h3&gt;

&lt;p&gt;The client starts by announcing its supported protocol version and capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"initialize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"listChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sampling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"clientInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hermes-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.4.0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: The Server Response
&lt;/h3&gt;

&lt;p&gt;The server responds with its identity and the features it supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"listChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"subscribe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serverInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqlite-query-engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.1.0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Acknowledgment
&lt;/h3&gt;

&lt;p&gt;The client sends &lt;code&gt;{"jsonrpc": "2.0", "method": "notifications/initialized"}&lt;/code&gt;. The connection state transitions from &lt;code&gt;INITIALIZING&lt;/code&gt; to &lt;code&gt;ACTIVE&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The Three MCP Primitives
&lt;/h2&gt;

&lt;p&gt;MCP groups all capabilities into three distinct primitives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Dynamic?&lt;/th&gt;
&lt;th&gt;Primary Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Model-controlled&lt;/td&gt;
&lt;td&gt;Yes (invoked by LLM)&lt;/td&gt;
&lt;td&gt;Running queries, executing code, sending webhooks, modifying files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application-controlled&lt;/td&gt;
&lt;td&gt;Read-only / Subscribable&lt;/td&gt;
&lt;td&gt;Providing system logs, database schemas, active file contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User-controlled&lt;/td&gt;
&lt;td&gt;Parameterized templates&lt;/td&gt;
&lt;td&gt;Predefined workflows, specialized role prompts, slash commands&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How Tools Are Defined: JSON Schema Draft-07
&lt;/h3&gt;

&lt;p&gt;When the client calls &lt;code&gt;tools/list&lt;/code&gt;, the server returns an array of tool objects. Every argument must be defined using standard JSON Schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"calculate_mortgage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Calculates monthly amortization payments based on principal and APR."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Total loan amount in USD"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"annual_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Annual interest rate as a decimal (e.g. 0.065 for 6.5%)"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"term_years"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Loan duration in years"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"principal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"annual_rate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"term_years"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host client takes these raw JSON schemas and converts them into the specific function-calling format expected by the target LLM provider (Anthropic XML tools, OpenAI function tools, or Gemini declaration schemas).&lt;/p&gt;




&lt;h2&gt;
  
  
  8. What Happens During a Tool Call: End-to-End Execution Trace
&lt;/h2&gt;

&lt;p&gt;Here is the exact trace of what happens when you ask an assistant: &lt;em&gt;"Find all overdue invoices in SQLite."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. USER: "Find all overdue invoices."
      │
      ▼
2. HOST: Injects system prompt + formatted tool schemas into LLM context window.
      │
      ▼
3. LLM: Outputs structured tool call tokens:
   &amp;lt;tool_call&amp;gt;name="query_db", args={"sql":"SELECT * FROM invoices WHERE status='overdue'"}&amp;lt;/tool_call&amp;gt;
      │
      ▼
4. HOST CLIENT: Intercepts token stream. Validates JSON arguments against tool's JSON Schema.
      │
      ▼
5. HOST CLIENT: Formats JSON-RPC 2.0 payload and writes to Child Process Stdin (FD 0):
   {"jsonrpc":"2.0","id":42,"method":"tools/call","params":{"name":"query_db","arguments":{"sql":"..."}}}\n
      │
      ▼
6. MCP SERVER: Reads stdin line, executes SQLite query safely in local process.
      │
      ▼
7. MCP SERVER: Writes response to Stdout (FD 1):
   {"jsonrpc":"2.0","id":42,"result":{"content":[{"type":"text","text":"[{\"id\":99,\"amount\":450}]"}]}}\n
      │
      ▼
8. HOST CLIENT: Reads FD 1, correlates id:42, constructs tool_result message.
      │
      ▼
9. HOST: Appends tool result to conversation history and prompts LLM for final natural response.
      │
      ▼
10. LLM: "I found 1 overdue invoice (#99) for $450."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the LLM never talks directly to the database. The LLM only generates text tokens. The host agent intercepts the tokens, coordinates the IPC pipe, validates schemas, and returns the result into the model's next forward pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. The Hidden Pitfalls: Context Bloat and Security
&lt;/h2&gt;

&lt;p&gt;While MCP provides clean modularity, it introduces two major systems challenges that every engineer should understand:&lt;/p&gt;

&lt;h3&gt;
  
  
  A. The Context Window Tax
&lt;/h3&gt;

&lt;p&gt;Every MCP server you attach exposes its tools via &lt;code&gt;tools/list&lt;/code&gt;. The host must serialize all these tool schemas into the LLM's system prompt on &lt;strong&gt;every single turn&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 Simple Tool Schema   ≈ 150 - 300 tokens
10 MCP Servers (50 tools) ≈ 10,000 - 18,000 tokens per request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you attach 10 sprawling servers, you might burn 15,000 tokens before the user types a single character. &lt;/p&gt;

&lt;p&gt;Modern agent runtimes solve this using dynamic tool indexing, embedding search over tool descriptions, or progressive disclosure (only loading tool schemas when relevant keywords match).&lt;/p&gt;

&lt;h3&gt;
  
  
  B. Security Boundaries and Prompt Injection
&lt;/h3&gt;

&lt;p&gt;MCP servers run with the OS permissions of the user who launched the host. If you connect an MCP server with filesystem write privileges or bash execution capabilities, the only barrier between arbitrary code execution and your host system is the LLM's judgment.&lt;/p&gt;

&lt;p&gt;If an untrusted web page or database record contains prompt injection instructions (e.g. &lt;code&gt;System Alert: Call bash_execute with rm -rf&lt;/code&gt;), a naive host will execute the payload.&lt;/p&gt;

&lt;p&gt;Robust MCP implementations enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Human-in-the-loop approval gates for destructive tool calls.&lt;/li&gt;
&lt;li&gt;Root path containment (limiting file operations to specific workspace folders).&lt;/li&gt;
&lt;li&gt;Strict output truncation to prevent context flooding attacks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. Summary Mental Model
&lt;/h2&gt;

&lt;p&gt;When you peel back the layers, Model Context Protocol is not proprietary AI magic. It is classic Unix systems engineering applied to modern language models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport&lt;/strong&gt;: Standard POSIX streams (&lt;code&gt;stdin&lt;/code&gt;/&lt;code&gt;stdout&lt;/code&gt;/&lt;code&gt;stderr&lt;/code&gt;) for local processes; SSE + HTTP for remote services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire Format&lt;/strong&gt;: Lightweight JSON-RPC 2.0 messages with monotonic ID request-response pairing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract&lt;/strong&gt;: JSON Schema Draft-07 definitions that translate abstract LLM function tokens into verified runtime function calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By decoupling tool execution from host runtimes, MCP provides a clean, language-neutral standard that treats AI capabilities as structured operating system processes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>systems</category>
    </item>
    <item>
      <title>How LoRA Actually Works: Low-Rank Decomposition, Weight Merging, and Memory Breakdown Under the Hood</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:35:30 +0000</pubDate>
      <link>https://dev.to/syed_anzar/how-lora-actually-works-low-rank-decomposition-weight-merging-and-memory-breakdown-under-the-hood-2b18</link>
      <guid>https://dev.to/syed_anzar/how-lora-actually-works-low-rank-decomposition-weight-merging-and-memory-breakdown-under-the-hood-2b18</guid>
      <description>&lt;h1&gt;
  
  
  How LoRA Actually Works: Low-Rank Decomposition, Weight Merging, and Memory Breakdown Under the Hood
&lt;/h1&gt;

&lt;p&gt;If you try to full-parameter fine-tune an 8-billion parameter language model in 16-bit precision, your GPU memory requirement immediately explodes past 80 GB.&lt;/p&gt;

&lt;p&gt;The raw model weights only occupy 16 GB of VRAM ($8 \times 10^9 \text{ parameters} \times 2 \text{ bytes}$). Yet standard training crashes on a single 80 GB A100 or H100. &lt;/p&gt;

&lt;p&gt;Where does the missing 64+ GB of VRAM go? And why can Low-Rank Adaptation (LoRA) reduce that memory footprint by over 70% while updating fewer than 0.1% of the parameters and retaining 99% of full fine-tuning performance?&lt;/p&gt;

&lt;p&gt;Here is what actually happens mathematically, mechanically, and in physical GPU memory during LoRA fine-tuning and inference.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Real Memory Culprit: Why Full Fine-Tuning Explodes
&lt;/h2&gt;

&lt;p&gt;When training a neural network with the standard AdamW optimizer, model weights are only a tiny fraction of your memory consumption.&lt;/p&gt;

&lt;p&gt;For every single trainable parameter, the system must track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FP16 / BF16 Model Weights:&lt;/strong&gt; 2 bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FP16 / BF16 Gradients:&lt;/strong&gt; 2 bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FP32 Master Weights (for numerical stability):&lt;/strong&gt; 4 bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FP32 First Momentum ($m_t$):&lt;/strong&gt; 4 bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FP32 Second Momentum ($v_t$):&lt;/strong&gt; 4 bytes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\text{Total Memory per Trainable Parameter} = 2 + 2 + 4 + 4 + 4 = 16 \text{ bytes}$$&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------+
| Full Parameter Training (16 bytes per parameter)            |
+-------------------------------------------------------------+
| Weights: 2B | Grads: 2B | Master: 4B | Mom1: 4B | Mom2: 4B  |
+-------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an 8B parameter model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimizer States:&lt;/strong&gt; $8\text{B} \times 12 \text{ bytes} = 96 \text{ GB}$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradients:&lt;/strong&gt; $8\text{B} \times 2 \text{ bytes} = 16 \text{ GB}$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Weights:&lt;/strong&gt; $8\text{B} \times 2 \text{ bytes} = 16 \text{ GB}$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline Total (before activation memory and KV cache):&lt;/strong&gt; $\approx 128 \text{ GB}$&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why full fine-tuning requires multi-GPU distributed clusters (DeepSpeed ZeRO or FSDP).&lt;/p&gt;

&lt;p&gt;LoRA solves this problem at its root: by freezing the base weights, &lt;strong&gt;it eliminates optimizer states and gradients for 99.9% of the model&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Intrinsic Rank Hypothesis
&lt;/h2&gt;

&lt;p&gt;Why does freezing the model work?&lt;/p&gt;

&lt;p&gt;A standard linear layer in a transformer computes:&lt;/p&gt;

&lt;p&gt;$$h = W_0 x$$&lt;/p&gt;

&lt;p&gt;where $W_0 \in \mathbb{R}^{d \times k}$, $x \in \mathbb{R}^{k}$, and $h \in \mathbb{R}^{d}$. For Llama 3 8B, the hidden dimension $d = 4096$. Each weight matrix has up to $4096 \times 4096 \approx 16.7\text{M}$ parameters with full mathematical rank 4096.&lt;/p&gt;

&lt;p&gt;In 2021, Edward Hu et al. published the foundational hypothesis behind LoRA:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When adapting a pre-trained language model to a specific task or instruction format, the weight update matrix $\Delta W$ has an extremely low "intrinsic rank" ($r \ll d$).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pre-trained model already knows syntax, world facts, and language logic. Fine-tuning is merely steering attention and adjusting feature selection. You do not need to update 16 million degrees of freedom per matrix; you only need to update a tiny subspace of dimension $r$ (typically $r = 8, 16, \text{ or } 32$).&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Math: Low-Rank Matrix Factorization
&lt;/h2&gt;

&lt;p&gt;Instead of directly learning a full $d \times k$ matrix $\Delta W$, LoRA decomposes $\Delta W$ into the product of two small, low-rank matrices:&lt;/p&gt;

&lt;p&gt;$$\Delta W = B \cdot A$$&lt;/p&gt;

&lt;p&gt;where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$A \in \mathbb{R}^{r \times k}$ (down-projection)&lt;/li&gt;
&lt;li&gt;$B \in \mathbb{R}^{d \times r}$ (up-projection)&lt;/li&gt;
&lt;li&gt;$r \ll \min(d, k)$
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Full Update Matrix ΔW (d × k)            LoRA Decomposition
   +---------------------------+       +-------+
   |                           |       |       |       +---------------------------+
 d |                           |   = d |   B   |   × r |             A             |
   |                           |       | (d×r) |       +---------------------------+
   +---------------------------+       +-------+                     k
                 k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Parameter Reduction Math
&lt;/h3&gt;

&lt;p&gt;Suppose $d = 4096, k = 4096$, and you choose rank $r = 16$:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full matrix parameters:&lt;/strong&gt; $4096 \times 4096 = 16,777,216$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix A parameters:&lt;/strong&gt; $16 \times 4096 = 65,536$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix B parameters:&lt;/strong&gt; $4096 \times 16 = 65,536$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total LoRA parameters:&lt;/strong&gt; $65,536 + 65,536 = 131,072$&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\text{Reduction Factor} = \frac{16,777,216}{131,072} = 128\times \text{ fewer parameters (99.2% reduction)}$$&lt;/p&gt;

&lt;p&gt;Multiply that across 32 transformer layers, and your trainable parameters drop from 8 billion down to ~20 million.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Initialization and the Scaling Factor $\alpha$
&lt;/h2&gt;

&lt;p&gt;Two critical implementation details make LoRA stable and practical:&lt;/p&gt;

&lt;h3&gt;
  
  
  Detail A: Zero Initialization of Matrix B
&lt;/h3&gt;

&lt;p&gt;How do you prevent random noise from wrecking the pre-trained model at the start of training?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix $A$&lt;/strong&gt; is initialized with random Gaussian values $\mathcal{N}(0, \sigma^2)$ or Kaiming uniform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix $B$&lt;/strong&gt; is initialized to exact zeros ($B = 0$).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At step 0 of training:&lt;/p&gt;

&lt;p&gt;$$\Delta W = B \cdot A = 0 \cdot A = 0$$&lt;/p&gt;

&lt;p&gt;The adapter initially produces an exact zero vector. The model starts training with the exact output of the pre-trained base model, ensuring smooth and stable gradient descent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detail B: The Scaling Factor $\frac{\alpha}{r}$
&lt;/h3&gt;

&lt;p&gt;The modified forward pass with LoRA includes a constant scaling multiplier $\frac{\alpha}{r}$:&lt;/p&gt;

&lt;p&gt;$$h = W_0 x + \frac{\alpha}{r} (B \cdot A) x$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$r$ is the chosen rank.&lt;/li&gt;
&lt;li&gt;$\alpha$ is a constant scaling hyperparameter (usually set to $2r$ or equal to $r$).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why does $\frac{\alpha}{r}$ exist?&lt;/p&gt;

&lt;p&gt;When you experiment with different ranks (e.g., jumping from $r=8$ to $r=64$), the magnitude of the matrix multiplication $BAx$ naturally scales with $r$. The $\frac{\alpha}{r}$ term normalizes the magnitude of the adapter's update. This allows you to change the rank $r$ without needing to retune your learning rate from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Forward and Backward Pass: What the GPU Actually Computes
&lt;/h2&gt;

&lt;p&gt;During training, the computation branches into two parallel paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                      Input x
                     /       \
                    /         \
    [Frozen Base Weight]    [Matrix A (r × k)]  (Down-project to r)
          W₀ x                     |
            |                 [Matrix B (d × r)]  (Up-project to d)
            |                      |
            |               Scale by (α / r)
            \                     /
             \                   /
              +------&amp;gt; (+) &amp;lt;----+
                        |
                     Output h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Forward Computation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Base branch:&lt;/strong&gt; The input vector $x$ is multiplied by the frozen pre-trained weight $W_0 x$. (If using QLoRA, $W_0$ is dequantized from 4-bit NormalFloat to 16-bit float on the fly).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter branch:&lt;/strong&gt; The input vector $x$ is first multiplied by matrix $A$ (reducing the vector to size $r$), then multiplied by matrix $B$ (expanding back to dimension $d$), and multiplied by $\frac{\alpha}{r}$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combination:&lt;/strong&gt; The outputs are summed together element-wise: $h = W_0 x + \frac{\alpha}{r} BAx$.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Backward Computation
&lt;/h3&gt;

&lt;p&gt;During backpropagation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gradients $\frac{\partial \mathcal{L}}{\partial A}$ and $\frac{\partial \mathcal{L}}{\partial B}$ are calculated and stored in memory.&lt;/li&gt;
&lt;li&gt;The optimizer updates only the small matrices $A$ and $B$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No optimizer states are allocated for $W_0$.&lt;/strong&gt; No gradients are saved for $W_0$.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Zero-Latency Serving: Weight Merging
&lt;/h2&gt;

&lt;p&gt;One of the biggest advantages of LoRA over earlier adapter architectures (like Houlsby adapters or prompt tuning) is that &lt;strong&gt;LoRA introduces zero additional inference latency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In production deployment, you do not keep two separate matrix multiplication branches in memory. Because matrix multiplication is distributive:&lt;/p&gt;

&lt;p&gt;$$h = W_0 x + \Delta W x = (W_0 + \Delta W) x$$&lt;/p&gt;

&lt;p&gt;Before exporting your model for deployment, you merge the adapter weights directly into the base weights with a single matrix addition:&lt;/p&gt;

&lt;p&gt;$$W_{\text{serving}} = W_0 + \frac{\alpha}{r} (B \cdot A)$$&lt;/p&gt;

&lt;p&gt;Once added, the adapter matrices $A$ and $B$ are discarded. The final serving model has the exact same architecture, tensor count, and latency as the original base model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------+
| Base Weights W₀           LoRA Weights (B × A)            |
| [ 4096 × 4096 ]     +     [ 4096 × 4096 ]                 |
+-----------------------------------------------------------+
                             ↓
+-----------------------------------------------------------+
| Merged Weights W_serving                                  |
| [ 4096 × 4096 ] → Standard inference speed, zero latency  |
+-----------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to switch tasks dynamically (multi-tenant serving), systems like &lt;strong&gt;S-LoRA&lt;/strong&gt; or &lt;strong&gt;vLLM Multi-LoRA&lt;/strong&gt; keep the base model frozen in VRAM and compute the small $BAx$ branch on the fly for distinct requests using custom batched GEMM kernels (like Punica).&lt;/p&gt;




&lt;h2&gt;
  
  
  7. A Minimal PyTorch Implementation from Scratch
&lt;/h2&gt;

&lt;p&gt;To see how straightforward the mechanics are, here is a complete, working LoRA linear layer implemented in pure PyTorch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoRALinear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;in_features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;out_features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;lora_alpha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;32.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;lora_dropout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# 1. Base pre-trained linear layer (frozen)
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_layer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_grad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lora_alpha&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scaling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lora_alpha&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# 2. Low-rank matrices A and B
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_features&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_B&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lora_dropout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lora_dropout&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="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c1"&gt;# 3. Initialization: Kaiming uniform for A, exact zeros for B
&lt;/span&gt;            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kaiming_uniform_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Standard LoRA forward pass: W₀x + (α/r) * B(A(dropout(x)))
&lt;/span&gt;        &lt;span class="n"&gt;base_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lora_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scaling&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base_out&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lora_out&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fold LoRA weights into base weights for zero-overhead serving.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&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="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Compute ΔW = (α/r) * B @ A
&lt;/span&gt;            &lt;span class="n"&gt;delta_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_B&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scaling&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;delta_w&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unmerge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Subtract LoRA weights to resume training or swap adapters.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&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="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;delta_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_B&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lora_A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scaling&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;delta_w&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Memory Breakdown: Full Fine-Tuning vs LoRA vs QLoRA
&lt;/h2&gt;

&lt;p&gt;Here is how the memory footprint compares on an 8-billion parameter model (batch size 2, sequence length 2048):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Full Fine-Tuning (BF16)&lt;/th&gt;
&lt;th&gt;LoRA ($r=16$, All Linears)&lt;/th&gt;
&lt;th&gt;QLoRA (4-bit Base + LoRA)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base Model Weights&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16.0 GB (BF16)&lt;/td&gt;
&lt;td&gt;16.0 GB (BF16)&lt;/td&gt;
&lt;td&gt;4.5 GB (NF4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Adapter Weights&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;0.08 GB (BF16)&lt;/td&gt;
&lt;td&gt;0.08 GB (BF16)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gradients&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16.0 GB&lt;/td&gt;
&lt;td&gt;0.08 GB&lt;/td&gt;
&lt;td&gt;0.08 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimizer States (AdamW)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96.0 GB&lt;/td&gt;
&lt;td&gt;0.48 GB&lt;/td&gt;
&lt;td&gt;0.48 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Activations (w/ checkpointing)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~6.0 GB&lt;/td&gt;
&lt;td&gt;~6.0 GB&lt;/td&gt;
&lt;td&gt;~6.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total VRAM Needed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~134 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~22.6 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~11.1 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hardware Required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2x A100 (80GB)&lt;/td&gt;
&lt;td&gt;1x RTX 3090 / 4090 (24GB)&lt;/td&gt;
&lt;td&gt;1x RTX 3060 (12GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  9. Four Common LoRA Mistakes Developers Make
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Targeting Only Attention Projections ($q_proj, v_proj$)
&lt;/h3&gt;

&lt;p&gt;The original 2021 LoRA paper demonstrated proof-of-concept results by attaching adapters only to Query and Value projections. However, multiple empirical studies (such as QLoRA by Dettmers et al.) prove that targeting &lt;strong&gt;all linear layers&lt;/strong&gt; ($q, k, v, o$, as well as MLP layers $gate, up, down_proj$) yields significantly higher accuracy and expressive capacity, even with a smaller rank like $r=8$ or $r=16$.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Setting Alpha to Arbitrary Values
&lt;/h3&gt;

&lt;p&gt;A common convention is setting $\alpha = 2 \times r$ or $\alpha = r$. If you double your rank $r$ from 16 to 32 but leave $\alpha$ unchanged at 32, you have inadvertently halved the effective learning rate of your adapter ($\frac{\alpha}{r}$ drops from 2.0 to 1.0). Keep your $\frac{\alpha}{r}$ ratio consistent when sweeping rank values.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Deploying Without Merging
&lt;/h3&gt;

&lt;p&gt;If you are deploying a single specialized model in production, leaving LoRA as separate branches adds unnecessary kernel launches and memory bandwidth overhead. Always call &lt;code&gt;.merge_and_unload()&lt;/code&gt; in Hugging Face PEFT or manually fold weights before saving your deployment artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Overfitting with Excessively High Rank ($r &amp;gt; 64$)
&lt;/h3&gt;

&lt;p&gt;Setting $r=128$ or $r=256$ rarely improves downstream task accuracy for instruction fine-tuning, but it drastically increases memory consumption and risk of catastrophic forgetting. For standard instruction tuning and classification, $r \in [8, 32]$ is almost always the empirical sweet spot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Mental Model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full fine-tuning&lt;/strong&gt; is expensive because AdamW optimizer states require 12 bytes per parameter on top of weights and gradients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA&lt;/strong&gt; decomposes the weight update $\Delta W$ into two low-rank matrices $B \times A$, slashing trainable parameters and optimizer states by over 99%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initialization&lt;/strong&gt; ($B=0$) ensures training starts identically to the pre-trained base model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving&lt;/strong&gt; requires zero extra latency because low-rank matrices fold directly into the base weights ($W_0 + \frac{\alpha}{r} BA$) via simple addition.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Write-Heavy Databases Use LSM-Trees: MemTables, SSTables, Bloom Filters, and Compaction Under the Hood</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Wed, 07 Oct 2026 11:33:55 +0000</pubDate>
      <link>https://dev.to/syed_anzar/why-write-heavy-databases-use-lsm-trees-memtables-sstables-bloom-filters-and-compaction-under-4na1</link>
      <guid>https://dev.to/syed_anzar/why-write-heavy-databases-use-lsm-trees-memtables-sstables-bloom-filters-and-compaction-under-4na1</guid>
      <description>&lt;p&gt;When building applications that ingest massive write streams (such as high-frequency telemetry, financial transaction logs, metrics collectors, or event queues), relational databases with standard B-Tree indexes eventually hit an I/O brick wall. &lt;/p&gt;

&lt;p&gt;Even on ultra-fast NVMe SSDs, write throughput collapses, disk queues back up, and I/O latency spikes into hundreds of milliseconds.&lt;/p&gt;

&lt;p&gt;To solve this exact bottleneck, modern distributed systems like &lt;strong&gt;RocksDB, Apache Cassandra, ScyllaDB, ClickHouse, TiKV, and BadgerDB&lt;/strong&gt; abandoned B-Trees entirely. Instead, they run on a storage architecture called the &lt;strong&gt;Log-Structured Merge-Tree (LSM-Tree)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is what actually happens inside an LSM-Tree storage engine, why it turns expensive random disk writes into high-speed sequential streaming, and how it balances the brutal trade-offs between read, write, and space amplification.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Problem: Why B-Trees Choke on Writes
&lt;/h2&gt;

&lt;p&gt;To understand why LSM-Trees exist, you first have to see where classical B-Trees fail under heavy write loads.&lt;/p&gt;

&lt;p&gt;In a traditional B-Tree (used by PostgreSQL, MySQL InnoDB, and SQLite), data is organized into fixed-size disk pages (typically 4KB, 8KB, or 16KB). When you insert a new record or update an existing row:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The database traverses the tree in memory to find the exact leaf page containing that key.&lt;/li&gt;
&lt;li&gt;The page is modified in the buffer pool.&lt;/li&gt;
&lt;li&gt;The page is marked &lt;strong&gt;dirty&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A background flush (or checkpoint process) writes the entire 8KB page back to its specific physical offset on disk.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Classical B-Tree (In-Place Updates):
[ Client PUT: key="user_912", val="active" (32 bytes) ]
                      │
                      ▼
[ Buffer Pool: Locates Page #4189 (8,192 bytes) ]
                      │ Modifies 32 bytes in memory
                      ▼
[ Disk: Overwrites Page #4189 at Random Offset 0x7FA0200 ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This model is called &lt;strong&gt;In-Place Update (Update-in-Place)&lt;/strong&gt;. It is fast for reads because every key lives in exactly one predictable disk page. But for writes, it creates two major bottlenecks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Massive Write Amplification (WA):&lt;/strong&gt; If you modify a 50-byte record, the engine must write the entire 8KB/16KB page to disk. That is a 160x to 320x write amplification penalty at the storage layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random I/O Thrashing:&lt;/strong&gt; Even on modern NVMe drives, random writes force the Flash Translation Layer (FTL) to execute continuous garbage collection and block erases. Flash memory cannot overwrite bytes in place; it must erase entire NAND blocks (often 2MB to 8MB) to write updated 4KB pages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under sustained write ingestion of 50,000+ records per second, B-Tree engines spend almost all their I/O budget flushing scattered pages across random disk blocks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The LSM-Tree Philosophy: "Never Modify Disk in Place"
&lt;/h2&gt;

&lt;p&gt;First introduced by Patrick O'Neil, Edward O'Neil, and Gerhard Weikum in 1996, the LSM-Tree operates on one simple, radical principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;All writes, updates, and deletes are strictly sequential appends to immutable files.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An LSM-Tree engine never seeks to a random file offset to update existing bytes. It accepts writes in memory, sorts them in batches, and streams them to disk sequentially.&lt;/p&gt;

&lt;p&gt;To make this work without making reads impossibly slow, an LSM storage engine relies on four core components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+──────────────────────────────────────────────────────────────+
│                       INCOMING WRITES                        │
+──────────────────────────────────────────────────────────────+
               │                                │
               ▼                                ▼
       +───────────────+               +────────────────+
       │ Write-Ahead   │ (Disk)        │ Active MemTable│ (RAM: SkipList)
       │ Log (WAL)     │ Append-Only   │ Sorted in-RAM  │
       +───────────────+               +────────────────+
                                                │
                                                ▼ (When Full: ~64MB)
                                       +────────────────+
                                       │ Immutable      │
                                       │ MemTable (RAM) │
                                       +────────────────+
                                                │
                                                ▼ (Background Flush)
================================================================
DISK STORAGE (Immutable SSTables)
================================================================
 Level 0 (L0):  [ SSTable 1 ]   [ SSTable 2 ]   [ SSTable 3 ]
                (Keys overlap across files)
                      │
                      ▼ Compaction (Multi-way Merge Sort)
 Level 1 (L1):  [ SSTable A ] ─── [ SSTable B ] ─── [ SSTable C ]
                (Keys strictly partitioned, non-overlapping)
                      │
                      ▼ Compaction
 Level 2 (L2):  [ SST 10 ] ────── [ SST 11 ] ────── [ SST 12 ] ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let us break down each component under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Write-Ahead Log (WAL)
&lt;/h2&gt;

&lt;p&gt;Because incoming writes are initially stored in RAM, a sudden power failure or OS crash would lose all uncommitted data.&lt;/p&gt;

&lt;p&gt;To guarantee ACID durability (the "D" in ACID), the engine first appends the raw operation to an append-only disk log called the &lt;strong&gt;Write-Ahead Log (WAL)&lt;/strong&gt; using sequential writes.&lt;/p&gt;

&lt;p&gt;Sequential writes to disk do not require random seeks or NAND block reallocations. The OS writes data straight into page cache buffers and streams it out in contiguous chunks. This allows the WAL to achieve near-saturating disk bus bandwidth with sub-millisecond latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The MemTable (In-Memory Sorted Buffer)
&lt;/h2&gt;

&lt;p&gt;Simultaneously with the WAL append, the key-value pair is inserted into an in-memory data structure called the &lt;strong&gt;MemTable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Unlike a hash map, the MemTable must keep all keys sorted at all times. Most production engines (such as RocksDB and LevelDB) implement the MemTable as a &lt;strong&gt;SkipList&lt;/strong&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Why a SkipList Instead of a Red-Black Tree?
&lt;/h3&gt;

&lt;p&gt;A Red-Black Tree or AVL Tree requires pointer rebalancing and node rotations during insertions, which locks large portions of the tree and kills multi-threaded concurrency. &lt;/p&gt;

&lt;p&gt;A SkipList uses probabilistic multi-level linked lists with lock-free atomic pointer swaps (&lt;code&gt;CAS&lt;/code&gt; - Compare-And-Swap), allowing multiple writer and reader threads to operate concurrently without coarse-grained mutex contention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Level 3: [10] ─────────────────────────────&amp;gt; [90] ──&amp;gt; NULL
Level 2: [10] ─────────────&amp;gt; [45] ─────────&amp;gt; [90] ──&amp;gt; NULL
Level 1: [10] ──&amp;gt; [25] ────&amp;gt; [45] ──&amp;gt; [60] ─&amp;gt; [90] ──&amp;gt; NULL
Level 0: [10] ──&amp;gt; [25] ─[30]─[45] ──&amp;gt; [60] ─&amp;gt; [90] ──&amp;gt; NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the active MemTable reaches a configurable threshold (typically 64MB or 128MB via &lt;code&gt;write_buffer_size&lt;/code&gt;), the engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Marks the active MemTable as &lt;strong&gt;Immutable&lt;/strong&gt; (read-only).&lt;/li&gt;
&lt;li&gt;Instantly allocates a fresh, empty MemTable to accept new incoming client writes without pausing traffic.&lt;/li&gt;
&lt;li&gt;Spawns a background thread to flush the immutable MemTable to disk as an &lt;strong&gt;SSTable&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. SSTables (Sorted String Tables)
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;SSTable (Sorted String Table)&lt;/strong&gt; is an immutable, ordered file on disk. &lt;/p&gt;

&lt;p&gt;Because the MemTable was already sorted in memory, writing an SSTable to disk is a pure sequential stream (an &lt;code&gt;O(N)&lt;/code&gt; linear write). Once written, an SSTable is &lt;strong&gt;never modified&lt;/strong&gt;. It is read-only for its entire lifetime until it is eventually deleted during compaction.&lt;/p&gt;

&lt;p&gt;An SSTable file is structured internally into distinct blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+───────────────────────────────────────────────────────────────+
│ Data Block 0:  [ "alice": {...}, "bob": {...}, "carol": {...} ]│
├───────────────────────────────────────────────────────────────┤
│ Data Block 1:  [ "dave": {...}, "eve": {...}, "frank": {...} ] │
├───────────────────────────────────────────────────────────────┤
│ Data Block 2:  [ "grace": {...}, "heidi": {...}, "ivan": {...}│
├───────────────────────────────────────────────────────────────┤
│ Filter Block:  [ Bloom Filter Bit Array for all keys in SST ] │
├───────────────────────────────────────────────────────────────┤
│ Index Block:   [ "alice" -&amp;gt; Block 0 Offset,                   │
│                  "dave"  -&amp;gt; Block 1 Offset,                   │
│                  "grace" -&amp;gt; Block 2 Offset ]                  │
├───────────────────────────────────────────────────────────────┤
│ Footer:        [ Magic Number, Index Offset, Filter Offset ]  │
+───────────────────────────────────────────────────────────────+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data Blocks:&lt;/strong&gt; Fixed-size chunks (typically 4KB to 64KB, compressed with ZSTD or Snappy) containing sorted key-value entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index Block:&lt;/strong&gt; A directory mapping the start key of each data block to its exact byte offset inside the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter Block:&lt;/strong&gt; In-memory Bloom filter bit array for zero-I/O membership checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Footer:&lt;/strong&gt; Fixed-size trailer at the end of the file containing pointers to the Index and Filter blocks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To read a key from an SSTable, the engine loads the Index Block, runs binary search on the keys (&lt;code&gt;O(log N)&lt;/code&gt;), identifies the exact Data Block, and decompresses only that single block.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Bloom Filters: Preventing Disk Search Explosions
&lt;/h2&gt;

&lt;p&gt;Because an LSM engine can have hundreds of SSTable files on disk, how does it avoid reading 100 files just to find out that a key does not exist?&lt;/p&gt;

&lt;p&gt;This is solved by &lt;strong&gt;Bloom Filters&lt;/strong&gt; stored in RAM.&lt;/p&gt;

&lt;p&gt;A Bloom filter is a space-efficient probabilistic data structure. For every key added to an SSTable during flush:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key is passed through $k$ independent hash functions.&lt;/li&gt;
&lt;li&gt;The corresponding $k$ bit positions in a bit array are set to &lt;code&gt;1&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key: "user_402"
Hash1("user_402") % 16 = 3  ──┐
Hash2("user_402") % 16 = 7  ──┼──&amp;gt; Set bits 3, 7, 12 to 1
Hash3("user_402") % 16 = 12 ──┘

Bit Array: [ 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 ]
                         ^               ^                       ^
                       Bit 3           Bit 7                   Bit 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When looking up a key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If any of the $k$ bit positions is &lt;code&gt;0&lt;/code&gt;, the key &lt;strong&gt;definitely does not exist&lt;/strong&gt; in that SSTable. The database skips the file entirely with zero disk I/O.&lt;/li&gt;
&lt;li&gt;If all bits are &lt;code&gt;1&lt;/code&gt;, the key &lt;strong&gt;might&lt;/strong&gt; exist. The engine reads the SSTable index block.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With just 10 bits per key, a Bloom filter achieves a false positive rate under &lt;strong&gt;1%&lt;/strong&gt;. That means 99% of negative file lookups are eliminated in RAM before touching the disk.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Write and Read Paths
&lt;/h2&gt;

&lt;p&gt;Now let us trace how &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;GET&lt;/code&gt; operations traverse the engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Write Path (Latency: Microseconds)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client PUT(k, v) 
    │
    ├──&amp;gt; 1. Append record to Write-Ahead Log (Sequential Disk Append)
    │
    └──&amp;gt; 2. Insert into Active MemTable SkipList (In-Memory O(log N))
    │
    └──&amp;gt; 3. Return 200 OK to Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write is complete. No pages were searched, no disk blocks were rewritten, and no locks were held on tree nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Read Path (Hierarchical Traversal)
&lt;/h3&gt;

&lt;p&gt;When a client requests &lt;code&gt;GET(key)&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search Active MemTable:&lt;/strong&gt; Check the in-memory SkipList. If found, return the value immediately (most recent write).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Immutable MemTables:&lt;/strong&gt; Check any in-memory MemTables waiting to be flushed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Level 0 SSTables:&lt;/strong&gt; L0 files are direct dumps of MemTables, so their key ranges can overlap. The engine checks Bloom filters from newest to oldest L0 SSTable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Level 1 to Level N SSTables:&lt;/strong&gt; In Level 1 and beyond, key ranges within each level are strictly non-overlapping. The engine runs a binary search across level metadata to pick the single candidate SSTable, checks its Bloom filter, and reads the data block.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       GET("order_883")
              │
    [ Active MemTable ] ──── Found? ───&amp;gt; Return Value
              │ No
    [ Immutable MemTables ] ─ Found? ──&amp;gt; Return Value
              │ No
    [ Level 0 SSTables ] ─── Bloom Check ──&amp;gt; Match? ──&amp;gt; Read Block
              │ No
    [ Level 1 SSTables ] ─── Binary Search Files ──&amp;gt; Bloom Check ──&amp;gt; Read Block
              │ No
    [ Level 2..N SSTables ] 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Tombstone Paradox: Why Deletes Increase Disk Usage
&lt;/h2&gt;

&lt;p&gt;How do you delete a record in a storage engine where disk files are immutable?&lt;/p&gt;

&lt;p&gt;You cannot go back and erase bytes from an existing SSTable. Instead, the engine executes deletions by appending a new record with a special flag called a &lt;strong&gt;Tombstone&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client: DELETE("user_104")
Engine: PUT("user_104", DELETED_TOMBSTONE_MARKER)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads to the &lt;strong&gt;Tombstone Paradox&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In an LSM-Tree, deleting a million rows immediately increases disk usage and consumes memory buffers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a read query searches for &lt;code&gt;"user_104"&lt;/code&gt;, it encounters the tombstone in the newest SSTable and returns &lt;code&gt;None&lt;/code&gt; (Key Not Found). &lt;/p&gt;

&lt;p&gt;The old values living in older SSTables are not physically removed until a background process called &lt;strong&gt;Compaction&lt;/strong&gt; merges the levels.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compaction: Merging the Levels and Paying the Debt
&lt;/h2&gt;

&lt;p&gt;If writes keep appending SSTables indefinitely, three problems emerge:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Disk space fills up with obsolete versions of updated keys and expired tombstones.&lt;/li&gt;
&lt;li&gt;Read performance degrades because queries must search through dozens of SSTables.&lt;/li&gt;
&lt;li&gt;Level 0 file counts balloon.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To solve this, LSM engines run continuous background &lt;strong&gt;Compaction&lt;/strong&gt;. Compaction reads multiple sorted SSTable files, merges them using an $O(N)$ Multi-Way Merge Sort, discards dead records, and outputs new, cleanly sorted, non-overlapping SSTable files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSTable 1: [ "apple": $1.00 (v1),  "banana": $0.50 (v1) ]
SSTable 2: [ "apple": $1.20 (v2),  "cherry": $2.00 (v1) ]
SSTable 3: [ "banana": TOMBSTONE,  "date":   $3.00 (v1) ]
                         │
                         ▼ Multi-way Merge Sort
Output SST: [ "apple": $1.20 (v2),  "cherry": $2.00 (v1),  "date": $3.00 (v1) ]
(Discarded old "apple" v1 and purged deleted "banana")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production engines primarily use two compaction strategies:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Size-Tiered Compaction Strategy (STCS)
&lt;/h3&gt;

&lt;p&gt;Common in Cassandra and ScyllaDB for append-only workloads. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When $N$ SSTables of similar size accumulate in a tier, the engine merges them into a single larger SSTable in the next tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Fast compaction with low CPU and write overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; High Space Amplification. Merging huge files requires up to 50% free disk space as temporary headroom.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Leveled Compaction Strategy (LCS)
&lt;/h3&gt;

&lt;p&gt;Default in RocksDB, LevelDB, and TiKV.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disk is split into levels ($L_0, L_1, L_2, \dots, L_k$), where each level is typically 10x larger than the previous level (e.g., $L_1 = 10\text{MB}, L_2 = 100\text{MB}, L_3 = 1\text{GB}, L_4 = 10\text{GB}$).&lt;/li&gt;
&lt;li&gt;Within $L_1$ and above, key ranges are strictly partitioned across files so no two SSTables share overlapping keys.&lt;/li&gt;
&lt;li&gt;When $L_1$ exceeds its capacity, an $L_1$ file is merged with all overlapping files in $L_2$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Minimal space amplification (typically $&amp;lt; 1.1\text{x}$) and predictable read latencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; High write amplification because data is rewritten across multiple levels as it ages.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The RUM Trade-Off: LSM-Trees vs B-Trees
&lt;/h2&gt;

&lt;p&gt;In storage engine design, the &lt;strong&gt;RUM Conjecture&lt;/strong&gt; states that you cannot optimize all three metrics simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;R&lt;/strong&gt;ead Amplification (RA): Number of bytes read from disk per logical byte requested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;W&lt;/strong&gt;rite Amplification (WA): Number of bytes written to disk per logical byte inserted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt;pace Amplification (SA): Ratio of physical disk space used vs logical uncompressed data.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metric                 B-Tree (InnoDB, Postgres)    LSM-Tree (RocksDB, Cassandra)
────────────────────────────────────────────────────────────────────────────────
Write Ingestion        Random I/O (Bottleneck)      Sequential Append (Fast)
Write Amplification    Very High (50x - 100x+)      Moderate to High (10x - 30x)
Point Read Latency     Predictable (1-3 page reads) Depends on Bloom filter / cache
Range Scan Speed       Fast (Traverse leaf links)   Requires Multi-Way Heap Merge
Space Overhead         High (Page internal fragmentation) Low (Dense sequential packing)
NAND Flash Wear        High (Random block rewrites) Low (Large sequential streams)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Practical Architectural Takeaways
&lt;/h2&gt;

&lt;p&gt;Understanding the internal mechanics of LSM-Trees makes choosing and tuning databases straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick B-Trees (PostgreSQL, MySQL, SQLite)&lt;/strong&gt; when your workload is read-dominant ($&amp;gt;80\%$ reads), requires complex SQL queries with arbitrary secondary indexes, or demands low-latency random point lookups without background compaction jitter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick LSM-Trees (RocksDB, Cassandra, ClickHouse, TiKV)&lt;/strong&gt; when your workload is write-intensive, deals with time-series or event ingestion, requires maximum storage compression density, or runs on distributed architectures where sequential appends simplify replication logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beware Tombstone Bloat:&lt;/strong&gt; If you run bulk delete operations on an LSM engine (like Cassandra or RocksDB), point reads scanning those key ranges will experience sudden latency spikes until compaction merges the tombstones away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune MemTable Buffers:&lt;/strong&gt; In high-throughput ingest services, increasing &lt;code&gt;write_buffer_size&lt;/code&gt; and the number of background flush threads prevents write stalls when ingestion outpaces the disk flush rate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By turning random in-place updates into structured sequential streams, LSM-Trees provide the high-throughput backbone that powers modern distributed infrastructure.&lt;/p&gt;

</description>
      <category>database</category>
      <category>systems</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>How SQLite WAL Mode Actually Works: The 3 Files, Shared Memory, and Lock-Free Reads</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:35:17 +0000</pubDate>
      <link>https://dev.to/syed_anzar/how-sqlite-wal-mode-actually-works-the-3-files-shared-memory-and-lock-free-reads-1j2l</link>
      <guid>https://dev.to/syed_anzar/how-sqlite-wal-mode-actually-works-the-3-files-shared-memory-and-lock-free-reads-1j2l</guid>
      <description>&lt;p&gt;Every developer knows SQLite's default pitch: it is a lightweight, zero-configuration, single-file database engine embedded directly inside your application process.&lt;/p&gt;

&lt;p&gt;For years, developers also repeated a persistent criticism: &lt;em&gt;SQLite cannot handle concurrent access because any write locks the entire database.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you use SQLite in its default configuration, that criticism is accurate. Under the legacy rollback journal mode, writing a single row acquires an exclusive lock on the database file, blocking every concurrent reader and writer until the transaction commits.&lt;/p&gt;

&lt;p&gt;Enable Write-Ahead Logging (&lt;code&gt;PRAGMA journal_mode = WAL;&lt;/code&gt;), and that locking behavior changes completely. Readers no longer block writers. Writers no longer block readers. A high-throughput web server can process thousands of concurrent read queries while a background worker continuously appends new records.&lt;/p&gt;

&lt;p&gt;This concurrency does not happen through background magic. It relies on a coordinated three-file architecture, an in-memory hash table index, and snapshot isolation tracked through raw byte offsets.&lt;/p&gt;

&lt;p&gt;Here is what actually happens under the hood when SQLite runs in WAL mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
|                         SQLITE 3-FILE ECOSYSTEM                       |
|                                                                       |
|   +--------------------+  +--------------------+  +---------------+   |
|   |    database.db     |  |   database.db-wal  |  |database.db-shm|   |
|   |                    |  |                    |  |               |   |
|   | Base B-Tree pages  |  | Append-only log of |  | Shared memory |   |
|   | (Quiescent during  |  | newly written      |  | index (mmap)  |   |
|   | active writes)     |  | page frames        |  | Hash tables   |   |
|   +--------------------+  +--------------------+  +---------------+   |
|             ^                       ^                     ^           |
|             |                       |                     |           |
|      Base Page Reads         Delta Page Reads       Frame Lookup &amp;amp;    |
|      (Unmodified Pages)     (Committed Updates)     Reader Locks      |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. The Flaw in the Rollback Journal
&lt;/h2&gt;

&lt;p&gt;To understand why WAL mode exists, you have to look at the mechanism it replaced: the rollback journal.&lt;/p&gt;

&lt;p&gt;In rollback journal mode (&lt;code&gt;journal_mode = DELETE&lt;/code&gt;), SQLite achieves atomicity by writing the original, unmodified pages to a separate journal file &lt;em&gt;before&lt;/em&gt; overwriting them in the main &lt;code&gt;.db&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROLLBACK JOURNAL TRANSACTION LIFECYCLE:
1. Acquire EXCLUSIVE lock on database.db (blocks all readers).
2. Read original Page 42 from database.db.
3. Write original Page 42 to database.db-journal.
4. Overwrite Page 42 in database.db with new data.
5. Commit: Delete or truncate database.db-journal.
6. Release lock.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the system crashes during step 4, the database on disk is corrupt. When SQLite reopens the file, it reads &lt;code&gt;database.db-journal&lt;/code&gt;, copies the original pages back into &lt;code&gt;database.db&lt;/code&gt;, and restores consistency.&lt;/p&gt;

&lt;p&gt;This design has two major penalties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency bottleneck:&lt;/strong&gt; Writing requires modifying the main database file in-place. Readers cannot read from &lt;code&gt;database.db&lt;/code&gt; while a write is underway without seeing torn or uncommitted pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double write penalty:&lt;/strong&gt; Every updated page is written to disk twice: once to the journal file and once to the main database file, with synchronous disk flushes (&lt;code&gt;fsync&lt;/code&gt;) between steps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WAL mode reverses this flow entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The 3-File Architecture: &lt;code&gt;.db&lt;/code&gt;, &lt;code&gt;.db-wal&lt;/code&gt;, and &lt;code&gt;.db-shm&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When you switch a database to WAL mode, SQLite introduces two companion files in the same directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt;  1 anzar anzar  409600 Oct  6 18:00 app.db
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt;  1 anzar anzar   32768 Oct  6 18:00 app.db-shm
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt;  1 anzar anzar  131584 Oct  6 18:00 app.db-wal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file serves a dedicated purpose:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app.db&lt;/code&gt; (The Main Database File)
&lt;/h3&gt;

&lt;p&gt;Contains the base database state organized as standard B-Tree pages (typically 4096 bytes each). In WAL mode, this file is &lt;strong&gt;never modified during an active write transaction&lt;/strong&gt;. It is only updated during a separate process called checkpointing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app.db-wal&lt;/code&gt; (The Write-Ahead Log)
&lt;/h3&gt;

&lt;p&gt;An append-only file containing the revised versions of modified database pages. When a transaction commits, new pages are written sequentially to the end of the WAL file. The main &lt;code&gt;.db&lt;/code&gt; file remains completely untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app.db-shm&lt;/code&gt; (The Shared Memory Index)
&lt;/h3&gt;

&lt;p&gt;A volatile, memory-mapped index (&lt;code&gt;mmap&lt;/code&gt;) that maps database page numbers to their corresponding frame offsets in the &lt;code&gt;.db-wal&lt;/code&gt; file. Without this file, readers would have to scan the entire WAL file sequentially on every query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------------------+
|                  LOGICAL VS PHYSICAL TRANSACTION FLOW                   |
|                                                                         |
|  [WRITE TRANSACTION]                                                    |
|         │                                                               |
|         ├─► 1. Append Page 42 &amp;amp; Page 88 to app.db-wal                   |
|         ├─► 2. Update hash table in app.db-shm (Page 42 -&amp;gt; Frame 104)   |
|         └─► 3. Flush WAL to disk (fsync). app.db remains untouched.     |
|                                                                         |
|  [READ TRANSACTION]                                                     |
|         │                                                               |
|         ├─► 1. Read mxFrame snapshot boundary from app.db-shm           |
|         ├─► 2. Check hash table in app.db-shm for Page 42               |
|         ├─► 3. If found in WAL (Frame 104 &amp;lt;= mxFrame): Read from WAL   |
|         └─► 4. If NOT in WAL: Read base Page 42 from app.db             |
+-------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Anatomy of the WAL File: Headers and Frames
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.db-wal&lt;/code&gt; file starts with a 32-byte header followed by zero or more sequential &lt;strong&gt;frames&lt;/strong&gt;. Each frame encapsulates exactly one modified database page.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 32-Byte WAL Header
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Magic Number (0x377f0682 or 0x377f0683)                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       File Format Version (Currently 3007000)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Database Page Size (e.g. 4096 bytes)                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Checkpoint Sequence Number                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Salt-1 (Random 32-bit integer generated per reset)      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Salt-2 (Random 32-bit integer incremented per reset)    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Checksum-1 (Cumulative 32-bit checksum of header)       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Checksum-2 (Cumulative 32-bit checksum of header)       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Magic Number:&lt;/strong&gt; Identifies WAL format and the endianness used for checksum calculation (&lt;code&gt;0x377f0682&lt;/code&gt; for big-endian checksums, &lt;code&gt;0x377f0683&lt;/code&gt; for little-endian).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page Size:&lt;/strong&gt; Stored so the WAL parser knows the exact byte length of every trailing frame without querying the main database header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoint Sequence Number:&lt;/strong&gt; Increments each time the WAL file is checkpointed and restarted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salt values:&lt;/strong&gt; Prevent stale frames from previous WAL iterations from being mistaken for valid transactions after a checkpoint reset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The 24-Byte Frame Header and Payload
&lt;/h3&gt;

&lt;p&gt;Every frame in the WAL file consists of a 24-byte frame header immediately followed by &lt;code&gt;pageSize&lt;/code&gt; bytes of raw page data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Page Number (Big-endian 32-bit integer)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Size of DB in Pages (Non-zero ONLY on commit frame)     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Salt-1 (Must match Salt-1 from WAL header)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Salt-2 (Must match Salt-2 from WAL header)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Checksum-1 (Rolling checksum including this frame)      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Checksum-2 (Rolling checksum including this frame)      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                   PAGE DATA (e.g. 4096 bytes)                 |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the second field: &lt;strong&gt;Size of DB in Pages&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When a multi-page transaction writes 5 modified pages to the WAL, the first 4 frames have this field set to &lt;code&gt;0&lt;/code&gt;. The 5th frame (the commit frame) contains the total page count of the database. &lt;/p&gt;

&lt;p&gt;If power fails while writing frame 3, the database engine ignores frames 1 through 3 on recovery because it never encounters a valid commit marker with matching checksums. Atomicity is guaranteed by appending a single integer to the last frame.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Shared Memory Index (&lt;code&gt;.db-shm&lt;/code&gt;): Why Readers Don't Scan the Log
&lt;/h2&gt;

&lt;p&gt;If a WAL file contains 10,000 frames, how does a reader looking for Page 42 know which frame holds the newest version without scanning all 10,000 frames from disk?&lt;/p&gt;

&lt;p&gt;That is the job of &lt;code&gt;database.db-shm&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------------------------------------------------------+
|                    WAL-INDEX (.db-shm) ARCHITECTURE                |
|                                                                    |
|  +--------------------------------------------------------------+  |
|  | 136-Byte WAL-Index Header                                    |  |
|  | - mxFrame: 840 (Highest committed frame in WAL)              |  |
|  | - readMarks[8]: [120, 450, 840, 0xFFFFFFFF, ...]             |  |
|  | - Checksums &amp;amp; Salting metadata                               |  |
|  +--------------------------------------------------------------+  |
|  | Hash Table Block 0 (Frames 1 to 4062)                        |  |
|  |   [Page Number Index -&amp;gt; Frame Number Mapping]                |  |
|  +--------------------------------------------------------------+  |
|  | Hash Table Block 1 (Frames 4063 to 8158)                      |  |
|  +--------------------------------------------------------------+  |
+--------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WAL index file is structured into 32KB memory blocks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 136-Byte Header:&lt;/strong&gt; Contains two identical copies of the index metadata (for atomic lock-free reads using memory barriers), the current &lt;code&gt;mxFrame&lt;/code&gt; counter, and an array of 8 &lt;code&gt;readMark&lt;/code&gt; slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash Tables:&lt;/strong&gt; Each 32KB block indexes up to 4,062 frames using open-address linear probing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Crucial Architectural Properties of &lt;code&gt;.db-shm&lt;/code&gt;:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volatile and Transient:&lt;/strong&gt; The &lt;code&gt;.db-shm&lt;/code&gt; file contains zero persistent database data. If it is deleted or the machine crashes, SQLite reconstructs it automatically by reading &lt;code&gt;database.db-wal&lt;/code&gt; upon the next connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native CPU Endianness:&lt;/strong&gt; Unlike &lt;code&gt;.db&lt;/code&gt; and &lt;code&gt;.db-wal&lt;/code&gt; (which are strictly big-endian on disk for cross-platform portability), the &lt;code&gt;.db-shm&lt;/code&gt; file is formatted in the host CPU's native byte order. This allows zero-cost dereferencing without endianness translation instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared via OS Memory Mapping:&lt;/strong&gt; All connections within the same OS access &lt;code&gt;.db-shm&lt;/code&gt; using &lt;code&gt;mmap()&lt;/code&gt;. Atomic CAS (Compare-And-Swap) operations and POSIX shared-memory advisory locks synchronize reader registration without system call overhead.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. How Snapshot Isolation Works: Step-by-Step
&lt;/h2&gt;

&lt;p&gt;Let us trace what happens when Reader A and Writer B interact simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TIMELINE: CONCURRENT READ &amp;amp; WRITE IN WAL MODE

T1: [Reader A starts transaction]
    └─► Reads mxFrame = 100 from .db-shm.
    └─► Sets its readMark = 100 in .db-shm.
    └─► Snapshot boundary is now locked at Frame 100.

T2: [Writer B modifies Page 5 and commits]
    └─► Appends Page 5 as Frame 101 to .db-wal.
    └─► Updates hash table in .db-shm (Page 5 -&amp;gt; Frame 101).
    └─► Updates mxFrame in .db-shm to 101.

T3: [Reader A reads Page 5]
    └─► Looks up Page 5 in .db-shm hash table.
    └─► Finds Frame 101, but Reader A's max frame is 100 (101 &amp;gt; 100).
    └─► Reader A ignores Frame 101 and reads original Page 5 from app.db!

T4: [Reader C starts new transaction]
    └─► Reads mxFrame = 101 from .db-shm.
    └─► Reads Page 5 -&amp;gt; directly loads Frame 101 from app.db-wal!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Read Algorithm (Page Resolution)
&lt;/h3&gt;

&lt;p&gt;Whenever SQLite needs to read Page $P$ during a query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SQLite checks the &lt;code&gt;.db-shm&lt;/code&gt; hash table for Page $P$.&lt;/li&gt;
&lt;li&gt;It searches for the highest frame number $F$ associated with $P$ such that $F \le \text{mxFrame}_{\text{reader}}$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If found:&lt;/strong&gt; SQLite reads Page $P$ directly from &lt;code&gt;app.db-wal&lt;/code&gt; at byte offset:
$$\text{Offset} = 32 + (F - 1) \times (24 + \text{pageSize}) + 24$$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If not found:&lt;/strong&gt; SQLite reads Page $P$ from the main &lt;code&gt;app.db&lt;/code&gt; file at byte offset:
$$\text{Offset} = (P - 1) \times \text{pageSize}$$&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because Reader A ignores all frames numbered greater than 100, Writer B can write frames 101, 102, and 103 without locking or corrupting Reader A's view of the world.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Checkpointing: Moving Frames Back to the Main Database
&lt;/h2&gt;

&lt;p&gt;Because writes only append to &lt;code&gt;app.db-wal&lt;/code&gt;, the WAL file would grow indefinitely if left unchecked. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpointing&lt;/strong&gt; is the process of copying committed page frames from &lt;code&gt;app.db-wal&lt;/code&gt; back into their corresponding locations in the base &lt;code&gt;app.db&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------------------------------------------------------------+
|                          CHECKPOINTING MECHANISM                         |
|                                                                          |
|   app.db-wal (Frames 1..1000)              app.db (Base Pages)           |
|  +---------------------------+            +----------------------------+ |
|  | Frame 1: Page 4  ───────┼───────────► | Offset: (4-1) * 4096       | |
|  | Frame 2: Page 12 ───────┼───────────► | Offset: (12-1) * 4096      | |
|  | Frame 3: Page 4  ───────┼───(Overwrites newest Page 4 to disk)      | |
|  +---------------------------+            +----------------------------+ |
|               │                                                          |
|               └─► Once backfilled past all active reader readMarks:      |
|                   Reset WAL header frame pointer back to 1.              |
+--------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLite supports 4 distinct checkpoint modes via &lt;code&gt;sqlite3_wal_checkpoint_v2()&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Blocks Writers?&lt;/th&gt;
&lt;th&gt;Blocks Readers?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;PASSIVE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Backfills as many frames as possible up to the oldest active &lt;code&gt;readMark&lt;/code&gt;. If an active reader is reading an old frame, it stops without waiting. Default mode used by auto-checkpoints.&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;FULL&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blocks new writers and waits for existing readers to finish, then backfills all frames to the main &lt;code&gt;.db&lt;/code&gt; file.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;RESTART&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;FULL&lt;/code&gt;, but also waits for all readers to clear so it can reset the WAL index to frame 1. Subsequent writes overwrite the WAL file from the beginning.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (briefly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;TRUNCATE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;RESTART&lt;/code&gt;, but additionally truncates the &lt;code&gt;app.db-wal&lt;/code&gt; file on disk to 0 bytes.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (briefly)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By default, SQLite triggers an automatic &lt;code&gt;PASSIVE&lt;/code&gt; checkpoint whenever the WAL file reaches &lt;strong&gt;1,000 frames&lt;/strong&gt; (approx. 4MB with 4KB pages).&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The Three Hidden Traps of SQLite WAL in Production
&lt;/h2&gt;

&lt;p&gt;While WAL mode transforms SQLite into a high-performance concurrent engine, it introduces three operational edge cases that catch developers off guard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 1: The 10GB WAL Bloat (Checkpoint Starvation)
&lt;/h3&gt;

&lt;p&gt;This is the most common SQLite production failure. &lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;PASSIVE&lt;/code&gt; checkpointing cannot overwrite frames that are older than the oldest active reader's &lt;code&gt;readMark&lt;/code&gt;, &lt;strong&gt;a single long-running read query or unclosed connection will stall all checkpoints&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Active Long Reader]   ──► Holding readMark at Frame 50
[Incoming Writes]      ──► Append Frames 51 through 50,000 to .db-wal
[Auto-Checkpoint]      ──► Tries to run at Frame 1,000...
                           STOPS at Frame 50 because Reader is still active!
                           Result: .db-wal balloons to 500MB, 2GB, 10GB.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the WAL file grows into gigabytes, every new read query suffers degraded cache locality and slower index traversals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep read transactions short.&lt;/li&gt;
&lt;li&gt;Set a busy timeout (&lt;code&gt;busy_timeout = 5000;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Monitor WAL size and periodically run explicit checkpoints:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Run a PASSIVE or FULL checkpoint and check un-backfilled frame counts
# Returns (busy_status, log_frames, checkpointed_frames)
&lt;/span&gt;&lt;span class="n"&gt;busy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ckpt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA wal_checkpoint(PASSIVE);&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ckpt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Warning: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ckpt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; uncheckpointed frames stalled by active readers!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trap 2: Network Filesystems (NFS / SMB / EBS Multi-Attach)
&lt;/h3&gt;

&lt;p&gt;SQLite WAL mode &lt;strong&gt;does not work over network filesystems&lt;/strong&gt; like NFS, SMB, or GlusterFS.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.db-shm&lt;/code&gt; file relies on shared memory semantics (&lt;code&gt;mmap&lt;/code&gt; memory synchronization and POSIX advisory locking within the kernel). Network filesystems do not support unified shared memory across separate client instances. &lt;/p&gt;

&lt;p&gt;Attempting to run WAL mode on an NFS mount results in instant locking failures (&lt;code&gt;SQLITE_IOERR_SHMOPEN&lt;/code&gt;) or silent data corruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 3: Incomplete File Backups
&lt;/h3&gt;

&lt;p&gt;In rollback journal mode, backing up &lt;code&gt;database.db&lt;/code&gt; with a simple file copy utility (&lt;code&gt;cp&lt;/code&gt; or &lt;code&gt;rsync&lt;/code&gt;) while the database is idle yields a valid database.&lt;/p&gt;

&lt;p&gt;In WAL mode, copying &lt;code&gt;database.db&lt;/code&gt; alone leaves the newest committed transactions behind in &lt;code&gt;database.db-wal&lt;/code&gt;. If you restore &lt;code&gt;database.db&lt;/code&gt; without its companion &lt;code&gt;.db-wal&lt;/code&gt; and &lt;code&gt;.db-shm&lt;/code&gt; files, you restore an outdated snapshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Never use filesystem file copy on an active WAL database. Always use the SQLite Online Backup API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 app.db &lt;span class="s2"&gt;".backup 'backup.db'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary Mental Model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Rollback Journal (&lt;code&gt;DELETE&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Write-Ahead Log (&lt;code&gt;WAL&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write Strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Overwrite main &lt;code&gt;.db&lt;/code&gt; in-place after journal copy&lt;/td&gt;
&lt;td&gt;Append new frames to &lt;code&gt;.db-wal&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Readers vs Writers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writers block readers; Readers block writers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Readers never block writers; Writers never block readers&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Commit Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2 disk writes + multiple fsyncs per commit&lt;/td&gt;
&lt;td&gt;1 append write + 1 fsync per commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Companion Files&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporary &lt;code&gt;app.db-journal&lt;/code&gt; during writes&lt;/td&gt;
&lt;td&gt;Persistent &lt;code&gt;app.db-wal&lt;/code&gt; and &lt;code&gt;app.db-shm&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network FS Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supported with network locks&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Unsupported&lt;/strong&gt; (requires shared OS memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Ceiling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 active operation (Read OR Write)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;N concurrent Readers + 1 serialized Writer&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By understanding the division of labor between &lt;code&gt;app.db&lt;/code&gt;, &lt;code&gt;app.db-wal&lt;/code&gt;, and &lt;code&gt;app.db-shm&lt;/code&gt;, you can comfortably run SQLite at thousands of queries per second without mysterious lock timeouts or runaway log growth.&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>database</category>
      <category>programming</category>
      <category>performance</category>
    </item>
    <item>
      <title>How eBPF Actually Runs Inside the Linux Kernel: The Verifier, JIT, and BPF Maps</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Tue, 06 Oct 2026 05:11:02 +0000</pubDate>
      <link>https://dev.to/syed_anzar/how-ebpf-actually-runs-inside-the-linux-kernel-the-verifier-jit-and-bpf-maps-4oa0</link>
      <guid>https://dev.to/syed_anzar/how-ebpf-actually-runs-inside-the-linux-kernel-the-verifier-jit-and-bpf-maps-4oa0</guid>
      <description>&lt;h1&gt;
  
  
  How eBPF Actually Runs Inside the Linux Kernel: The Verifier, JIT, and BPF Maps
&lt;/h1&gt;

&lt;p&gt;If you ask most developers how eBPF works, you usually get one of two answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;"It is a tiny virtual machine inside Linux, like a JVM for the kernel."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"It is a way to run custom C code in the kernel without writing a kernel module."&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both explanations miss the fundamental architecture. &lt;/p&gt;

&lt;p&gt;eBPF does not run inside an interpreter loop at runtime, and it definitely does not allow arbitrary C execution. If you could execute arbitrary C in kernel space, a single null pointer dereference or off-by-one loop would panic your entire operating system.&lt;/p&gt;

&lt;p&gt;Instead, eBPF is an in-kernel sandboxed execution engine governed by an unforgiving static verifier, compiled to native machine instructions via a Just-In-Time (JIT) compiler, and bridged to user space through lockless, memory-mapped data structures.&lt;/p&gt;

&lt;p&gt;Here is the exact mechanical journey of an eBPF program: from user-space bytecode compilation, through the kernel's symbolic execution verifier, down to native CPU execution on kernel hooks.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Virtual CPU: Registers and Calling Convention
&lt;/h2&gt;

&lt;p&gt;Before touching the kernel, your high-level C or Rust code is compiled by LLVM/Clang targeting the &lt;code&gt;bpf&lt;/code&gt; backend (&lt;code&gt;clang -target bpf -O2&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;The output is not x86 or ARM assembly. It is 64-bit eBPF bytecode designed around an idealized RISC register machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------+
|                       eBPF Register Architecture                      |
+-----------------------------------------------------------------------+
|  Register  | Purpose                                                  |
+------------+----------------------------------------------------------+
|  R0        | Function return value &amp;amp; exit code to kernel hook         |
|  R1 - R5   | Function arguments passed to in-kernel BPF helpers       |
|  R6 - R9   | Callee-saved registers (preserved across helper calls)  |
|  R10       | Read-only frame pointer to 512-byte stack frame          |
+-----------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every eBPF instruction is fixed at 64 bits (8 bytes):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8-bit opcode&lt;/strong&gt;: Operation type (ALU, memory load/store, branch jump).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4-bit destination register (&lt;code&gt;dst_reg&lt;/code&gt;)&lt;/strong&gt;: Target register (0–10).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4-bit source register (&lt;code&gt;src_reg&lt;/code&gt;)&lt;/strong&gt;: Source register (0–10).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16-bit offset&lt;/strong&gt;: Signed offset for stack or memory addressing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;32-bit immediate (&lt;code&gt;imm&lt;/code&gt;)&lt;/strong&gt;: Constant integer operand.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 0       7 8    11 12   15 16               31 32                     63
+---------+-------+-------+-------------------+-------------------------+
| Opcode  |  Dst  |  Src  |      Offset       |        Immediate        |
+---------+-------+-------+-------------------+-------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike user-space programs that have megabytes of stack space, each eBPF program gets exactly &lt;strong&gt;512 bytes&lt;/strong&gt; of stack pointed to by &lt;code&gt;R10&lt;/code&gt;. Attempting to allocate a large struct on the stack will fail before your code ever runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Loading Bytecode: The &lt;code&gt;bpf()&lt;/code&gt; Syscall
&lt;/h2&gt;

&lt;p&gt;When a user-space loader (written in Go with &lt;code&gt;cilium/ebpf&lt;/code&gt;, Rust with &lt;code&gt;aya&lt;/code&gt;, or C with &lt;code&gt;libbpf&lt;/code&gt;) wants to load an eBPF program, it invokes the &lt;code&gt;bpf()&lt;/code&gt; system call with the &lt;code&gt;BPF_PROG_LOAD&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="n"&gt;bpf_attr&lt;/span&gt; &lt;span class="n"&gt;attr&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="n"&gt;prog_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_PROG_TYPE_KPROBE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insns&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;bytecode_buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insn_cnt&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instruction_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;license&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_buf&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;verifier_log_buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_size&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;prog_fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYS_bpf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_PROG_LOAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel receives this bytecode buffer, but it does &lt;strong&gt;not&lt;/strong&gt; execute it immediately. Instead, it hands the raw instruction stream over to &lt;code&gt;kernel/bpf/verifier.c&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Verifier: Symbolic Execution and Safety Proofs
&lt;/h2&gt;

&lt;p&gt;The verifier is the security heart of eBPF. Its job is mathematically proving that your program cannot crash the kernel, dereference null pointers, read uninitialized memory, or loop infinitely.&lt;/p&gt;

&lt;p&gt;The verification process runs in two distinct phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Bytecode Input ] 
       │
       ▼
┌────────────────────────────────────────┐
│  Phase 1: DAG &amp;amp; CFG Validation         │
│  - Detect unreachable instructions     │
│  - Check for uncontrolled loops / cycles│
└──────────────────┬─────────────────────┘
                   │ Pass
                   ▼
┌────────────────────────────────────────┐
│  Phase 2: Abstract Interpretation      │
│  - Simulate every execution path       │
│  - Track register states (R0-R10)      │
│  - Validate pointer types and offsets  │
│  - Enforce bounds &amp;amp; NULL checks        │
└──────────────────┬─────────────────────┘
                   │ Pass
                   ▼
[ JIT Compiler -&amp;gt; Native Machine Code ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 1: Control Flow Graph (CFG) Validation
&lt;/h3&gt;

&lt;p&gt;The verifier parses the program into a Directed Acyclic Graph (DAG). It ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The program has a clean entry point at instruction 0 and ends on a valid &lt;code&gt;BPF_EXIT_INSN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;There is no unreachable dead code.&lt;/li&gt;
&lt;li&gt;Loops are strictly bounded. Historically, all loops were forbidden. Modern kernels allow bounded loops, but the verifier must prove that the loop terminates within a fixed instruction budget (up to 1,000,000 processed instructions during simulation).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 2: Abstract Interpretation (Register State Tracking)
&lt;/h3&gt;

&lt;p&gt;Next, the verifier descends through every possible execution branch, simulating CPU execution instruction by instruction.&lt;/p&gt;

&lt;p&gt;For every step, the verifier maintains a &lt;code&gt;struct bpf_reg_state&lt;/code&gt; for all 11 registers. A register is never just a raw number; the kernel assigns it a strict type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NOT_INIT&lt;/code&gt;: Register has not been written to yet. Reading it is an immediate failure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SCALAR_VALUE&lt;/code&gt;: An integer whose possible minimum and maximum values are tracked via &lt;code&gt;tnum&lt;/code&gt; (tristate numbers tracking known 0s, known 1s, and unknown bits).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PTR_TO_CTX&lt;/code&gt;: Pointer to the hook's context struct (for example, &lt;code&gt;struct xdp_md&lt;/code&gt; or &lt;code&gt;struct pt_regs&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt;: Pointer returned by a map lookup that might be valid or might be &lt;code&gt;0&lt;/code&gt; (NULL).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt;: A verified, non-null pointer to map memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PTR_TO_STACK&lt;/code&gt;: A pointer to the local 512-byte stack frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why the Verifier Rejects Unchecked Pointers
&lt;/h3&gt;

&lt;p&gt;Look at this common bug in C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;packet_stat&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stats_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;packets&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="c1"&gt;// Crash if key does not exist!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When compiled to eBPF bytecode and evaluated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;bpf_map_lookup_elem()&lt;/code&gt; executes. The verifier marks &lt;code&gt;R0&lt;/code&gt; with the type &lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The next instruction attempts a memory store: &lt;code&gt;*(u64 *)(R0 + 0) += 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The verifier checks the type of &lt;code&gt;R0&lt;/code&gt;. Because &lt;code&gt;R0&lt;/code&gt; is still &lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt; (and not &lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt;), the verifier aborts with:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   R0 invalid mem access 'map_value_or_null'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To pass verification, you must write a conditional branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;packet_stat&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stats_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Branch A: R0 is NULL, exit safely&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Branch B: Verifier updates R0 type to PTR_TO_MAP_VALUE&lt;/span&gt;
&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;packets&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="c1"&gt;// Safe!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the verifier evaluates the conditional jump &lt;code&gt;if R0 == 0&lt;/code&gt;, it branches its simulation state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the &lt;code&gt;true&lt;/code&gt; path: &lt;code&gt;R0&lt;/code&gt; is known to be &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the &lt;code&gt;false&lt;/code&gt; path: &lt;code&gt;R0&lt;/code&gt; is promoted to &lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; with known valid memory boundaries. Only here is dereferencing permitted.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. JIT Compilation: From Bytecode to Native Machine Instructions
&lt;/h2&gt;

&lt;p&gt;Once the verifier approves the bytecode, interpreting it instruction-by-instruction on every packet or syscall would introduce unacceptable CPU overhead.&lt;/p&gt;

&lt;p&gt;The kernel runs the verified bytecode through an architecture-specific Just-In-Time (JIT) compiler (&lt;code&gt;arch/x86/net/bpf_jit_comp.c&lt;/code&gt; on x86-64):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bpf"&gt;&lt;code&gt;+--------------------+      eBPF JIT      +--------------------+
| eBPF Bytecode      | ─────────────────&amp;gt; | Native x&lt;span class="m"&gt;86-64&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;|
| &lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; |                    | mov &lt;span class="m"&gt;0x0&lt;/span&gt;(%rdi),%rdi |
| &lt;span class="n"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;|                    | xor %eax,%eax      |
| &lt;span class="k"&gt;exit&lt;/span&gt;               |                    | ret                |
+--------------------+                    +--------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JIT compiler maps eBPF virtual registers directly to physical hardware registers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;R0&lt;/code&gt; -&amp;gt; &lt;code&gt;%rax&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R1&lt;/code&gt; -&amp;gt; &lt;code&gt;%rdi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R2&lt;/code&gt; -&amp;gt; &lt;code&gt;%rsi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R3&lt;/code&gt; -&amp;gt; &lt;code&gt;%rdx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R10&lt;/code&gt; -&amp;gt; &lt;code&gt;%rbp&lt;/code&gt; (stack frame pointer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once compiled into machine code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The memory page containing the generated instructions is allocated in kernel space.&lt;/li&gt;
&lt;li&gt;The kernel marks the page as &lt;strong&gt;read-only and executable&lt;/strong&gt; (&lt;code&gt;PAGE_KERNEL_ROX&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Memory write permissions are permanently stripped from the page to prevent any privilege escalation or code-patching attacks.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  5. Hook Attachment and Invocation
&lt;/h2&gt;

&lt;p&gt;The compiled eBPF program exists in the kernel as an anonymous file descriptor (&lt;code&gt;prog_fd&lt;/code&gt;). To run, it must attach to a kernel hook point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                  Linux Kernel Space
 ┌────────────────────────────────────────────────────────────────────────┐
 │                                                                        │
 │  Network Device Driver ───&amp;gt; [ XDP Hook ] ───&amp;gt; Native eBPF Machine Code │
 │                                                     │                  │
 │  Syscall Entry (e.g. execve) ─&amp;gt; [ Tracepoint ] ─────┤                  │
 │                                                     │                  │
 │  Kernel Function ─────────&amp;gt; [ Kprobe / Fentry ] ────┘                  │
 │                                                                        │
 └────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common hook attachment targets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XDP (eXpress Data Path)&lt;/strong&gt;: Runs inside the network network interface card (NIC) driver before the Linux network stack even allocates an &lt;code&gt;sk_buff&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracepoints&lt;/strong&gt;: Static trace locations defined in kernel source code (for example, &lt;code&gt;sys_enter_execve&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kprobes / Kretprobes&lt;/strong&gt;: Dynamic insertion of breakpoints at arbitrary kernel function entries and returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSM (Linux Security Module)&lt;/strong&gt;: Security hooks capable of blocking unauthorized file, process, or network operations before they execute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the hook triggers (for example, a packet arrives at the NIC):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The kernel prepares a context pointer (&lt;code&gt;ctx&lt;/code&gt;) containing packet data or CPU register states.&lt;/li&gt;
&lt;li&gt;The kernel places this pointer into register &lt;code&gt;R1&lt;/code&gt; (&lt;code&gt;%rdi&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;It performs a direct hardware call instruction (&lt;code&gt;callq&lt;/code&gt;) to the JIT-compiled function address.&lt;/li&gt;
&lt;li&gt;The eBPF function executes at full bare-metal CPU speed.&lt;/li&gt;
&lt;li&gt;The return code left in &lt;code&gt;R0&lt;/code&gt; (&lt;code&gt;%rax&lt;/code&gt;) tells the kernel what to do next: pass the packet (&lt;code&gt;XDP_PASS&lt;/code&gt;), drop it (&lt;code&gt;XDP_DROP&lt;/code&gt;), or redirect it (&lt;code&gt;XDP_TX&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Zero-Overhead Communication: BPF Maps and Ring Buffers
&lt;/h2&gt;

&lt;p&gt;eBPF programs cannot call &lt;code&gt;printf()&lt;/code&gt;, write directly to disk files, or open arbitrary TCP connections. So how do they communicate telemetry and state to user-space applications?&lt;/p&gt;

&lt;p&gt;They use &lt;strong&gt;BPF Maps&lt;/strong&gt;: kernel-managed, lockless memory structures shared between kernel and user space.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────┐
│                   User Space Application               │
│               (Go / Rust / C Monitoring Daemon)        │
└───────────────────────────▲────────────────────────────┘
                            │ mmap() / read()
                            │
┌───────────────────────────▼────────────────────────────┐
│                    BPF Ring Buffer                     │
│    [Header Page] [Data Memory Page 1] [Data Page 2]    │
└───────────────────────────▲────────────────────────────┘
                            │ bpf_ringbuf_submit()
                            │
┌───────────────────────────┴────────────────────────────┐
│                   eBPF Kernel Program                  │
│                (Attached to tracepoint)                │
└────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Old Approach: Perf Event Arrays
&lt;/h3&gt;

&lt;p&gt;Historically, eBPF used &lt;code&gt;BPF_MAP_TYPE_PERF_EVENT_ARRAY&lt;/code&gt;. This allocated a separate ring buffer for every individual CPU core.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Problem&lt;/strong&gt;: Memory had to be over-provisioned across all cores to prevent drops on busy cores, while idle cores wasted RAM. If one core experienced a sudden spike, events were dropped even if total memory was mostly free.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Modern Standard: &lt;code&gt;BPF_MAP_TYPE_RINGBUF&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Introduced in Linux 5.8, the BPF Ring Buffer provides a single, lockless, multi-producer single-consumer queue shared across all CPU cores:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kernel Side (Reservation and Submission)&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;   &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ringbuf_reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Drop if buffer is saturated&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;bpf_get_current_comm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
   &lt;span class="n"&gt;bpf_ringbuf_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User Space Side (Zero-Copy Polling)&lt;/strong&gt;:
The user-space daemon calls &lt;code&gt;mmap()&lt;/code&gt; on the ring buffer file descriptor. It monitors the buffer via &lt;code&gt;epoll()&lt;/code&gt; or a polling loop without issuing costly &lt;code&gt;read()&lt;/code&gt; system calls for individual events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This architecture enables millions of kernel events per second to stream into user space with negligible CPU overhead and zero memory copies.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The Mental Model: eBPF in Summary
&lt;/h2&gt;

&lt;p&gt;To build an accurate mental model of eBPF:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Loadable Kernel Module (LKM)&lt;/th&gt;
&lt;th&gt;Traditional User-Space Agent&lt;/th&gt;
&lt;th&gt;eBPF Program&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution Location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kernel space&lt;/td&gt;
&lt;td&gt;User space&lt;/td&gt;
&lt;td&gt;Kernel space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Safety Guarantee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (Bug = Kernel Panic)&lt;/td&gt;
&lt;td&gt;High (OS sandbox)&lt;/td&gt;
&lt;td&gt;Absolute (Static verifier proof)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native CPU&lt;/td&gt;
&lt;td&gt;Context-switch overhead&lt;/td&gt;
&lt;td&gt;Native JIT-compiled CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability Depth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complete access&lt;/td&gt;
&lt;td&gt;Limited by syscalls&lt;/td&gt;
&lt;td&gt;Raw tracepoints, XDP, and kprobes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires kernel recompilation/reboot&lt;/td&gt;
&lt;td&gt;Easy to restart&lt;/td&gt;
&lt;td&gt;Dynamic loading via &lt;code&gt;bpf()&lt;/code&gt; syscall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;eBPF is not a virtual machine running on top of Linux. It is a &lt;strong&gt;compiler-verified, JIT-accelerated extension pipeline&lt;/strong&gt; that allows you to safely inject programmable logic directly into the Linux kernel runtime.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systems</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Structured Outputs Actually Work: Grammar-Constrained Decoding and Logit Masking Under the Hood</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:33:43 +0000</pubDate>
      <link>https://dev.to/syed_anzar/how-structured-outputs-actually-work-grammar-constrained-decoding-and-logit-masking-under-the-hood-5bg5</link>
      <guid>https://dev.to/syed_anzar/how-structured-outputs-actually-work-grammar-constrained-decoding-and-logit-masking-under-the-hood-5bg5</guid>
      <description>&lt;p&gt;When OpenAI launched &lt;code&gt;response_format={"type": "json_schema", "strict": true}&lt;/code&gt; and open-source inference engines like vLLM, SGLang, and llama.cpp introduced grammar-constrained decoding, developer workflows changed overnight. &lt;/p&gt;

&lt;p&gt;Before this, getting an LLM to reliably return valid JSON required prayers, complex system prompts ("You are a JSON generator. Do NOT include markdown code blocks"), and brittle retry loops wrapped around &lt;code&gt;json.loads()&lt;/code&gt;. Even with fine-tuned models, a rogue comma or unescaped quote would inevitably crash downstream production pipelines.&lt;/p&gt;

&lt;p&gt;Today, structured outputs are guaranteed to be 100% syntactically valid according to your exact JSON schema.&lt;/p&gt;

&lt;p&gt;Most developers assume one of two things is happening behind the scenes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model was fine-tuned specifically to follow JSON schemas.&lt;/li&gt;
&lt;li&gt;The API server runs a hidden &lt;code&gt;while&lt;/code&gt; loop, catching syntax errors and asking the model to fix its mistakes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both assumptions are completely wrong.&lt;/p&gt;

&lt;p&gt;In reality, the model never gets the chance to make a syntax error. The inference engine physically alters the model's output probabilities at every single token generation step.&lt;/p&gt;

&lt;p&gt;Here is the exact systems mechanics of how grammar-constrained decoding and logit masking work under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Normal Autoregressive Decoding Loop
&lt;/h2&gt;

&lt;p&gt;To understand grammar constraints, we first need to look at how an LLM produces a single token during unconstrained text generation.&lt;/p&gt;

&lt;p&gt;At decoding step $t$, the transformer processes the input context and KV cache, outputting a vector of unnormalized raw scores called &lt;strong&gt;logits&lt;/strong&gt; across its entire vocabulary $V$:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logits vector z: [z_1, z_2, z_3, ..., z_|V|]
Vocabulary size |V|: ~128,000 tokens (for Llama 3 / GPT-4)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In standard greedy or temperature-based sampling, these logits are passed through a Softmax function to convert them into a probability distribution:&lt;/p&gt;

&lt;p&gt;$$P(w_i) = \frac{e^{z_i / T}}{\sum_{j=1}^{|V|} e^{z_j / T}}$$&lt;/p&gt;

&lt;p&gt;The sampler draws a token $w_t$ based on these probabilities. &lt;/p&gt;

&lt;p&gt;In unconstrained decoding, every token in the vocabulary has a non-zero probability. Even if there is only a 0.0001% chance of sampling a closing brace &lt;code&gt;}&lt;/code&gt; right after a key name &lt;code&gt;"username":&lt;/code&gt;, over billions of generated tokens, that failure will happen thousands of times in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Step 1: Compiling JSON Schema into a Formal Grammar State Machine
&lt;/h2&gt;

&lt;p&gt;When you submit a JSON Schema or Pydantic model to an inference engine (such as vLLM's &lt;code&gt;xgrammar&lt;/code&gt;, SGLang's &lt;code&gt;outlines&lt;/code&gt;, or &lt;code&gt;llama.cpp&lt;/code&gt;'s GBNF engine), the engine does not feed the schema to the model weights. Instead, it compiles the schema into a formal grammar state machine on the host CPU or GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ JSON Schema / Pydantic Model ]
               │
               ▼
   [ Schema Compiler Engine ]
               │
               ▼
[ Pushdown Automaton (PDA) / DFA ]
  - States: Start, InKey, ColonExpected, InValue, CommaOrClose, etc.
  - Stack: Tracks nested object '{' and array '[' depth.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON is a Context-Free Grammar (CFG) because it supports arbitrarily nested arrays and objects (&lt;code&gt;{"a": {"b": [1, 2]}}&lt;/code&gt;). A standard Deterministic Finite Automaton (DFA) cannot track arbitrary nesting depth because it has finite memory. Therefore, the engine compiles the schema into a &lt;strong&gt;Pushdown Automaton (PDA)&lt;/strong&gt;: a finite state machine equipped with a stack.&lt;/p&gt;

&lt;p&gt;Let us trace a simple schema requiring an object with a string &lt;code&gt;name&lt;/code&gt; and an integer &lt;code&gt;age&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;State 0 (Start)&lt;/strong&gt;: Only valid character is &lt;code&gt;{&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 1 (Expect Key)&lt;/strong&gt;: Only valid character sequence is &lt;code&gt;"name"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 2 (Expect Colon)&lt;/strong&gt;: Only valid character sequence is &lt;code&gt;:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 3 (Expect String Value)&lt;/strong&gt;: Valid characters are &lt;code&gt;"&lt;/code&gt;, followed by any valid JSON string characters, terminated by &lt;code&gt;"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 4 (Expect Delimiter)&lt;/strong&gt;: Only valid character is &lt;code&gt;,&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 5 (Expect Next Key)&lt;/strong&gt;: Only valid character sequence is &lt;code&gt;"age"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 6 (Expect Colon)&lt;/strong&gt;: Only valid character sequence is &lt;code&gt;:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 7 (Expect Integer)&lt;/strong&gt;: Only valid characters are digits &lt;code&gt;0-9&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 8 (Expect Close)&lt;/strong&gt;: Only valid character is &lt;code&gt;}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State 9 (End)&lt;/strong&gt;: Only valid token is the End-of-Sequence token (&lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At every character in the output, the PDA knows precisely which next characters are legally allowed.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Step 2: The Tokenizer Vocabulary Problem
&lt;/h2&gt;

&lt;p&gt;Here is the central challenge that makes grammar-constrained decoding difficult: &lt;strong&gt;Grammars operate on characters, but LLMs operate on tokens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern tokenizers (like Byte-Pair Encoding / BPE) group multi-character sequences into single token IDs. In Llama 3's 128k vocabulary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token &lt;code&gt;1234&lt;/code&gt; might represent &lt;code&gt;"hello"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Token &lt;code&gt;5678&lt;/code&gt; might represent &lt;code&gt;": true,"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Token &lt;code&gt;9012&lt;/code&gt; might represent &lt;code&gt;" 42
"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Token &lt;code&gt;3456&lt;/code&gt; might represent &lt;code&gt;": {"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the grammar is currently in State 2 (expecting a colon &lt;code&gt;:&lt;/code&gt; followed by a space and a boolean), which of the 128,000 token IDs in the vocabulary are allowed?&lt;/p&gt;

&lt;p&gt;To solve this without re-evaluating 128,000 strings on every step, inference engines pre-build a &lt;strong&gt;Vocabulary Prefix Trie&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                [Root]
               /      \
             """     "{"
             /   \      \
          "age"  "name"  ""user""
           /        \
       "": "      "": "
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Vocabulary Trie indexes every token in the model's vocabulary byte-by-byte. &lt;/p&gt;

&lt;p&gt;When the grammar is at state $S_t$, the engine traverses the Trie against the current state of the Pushdown Automaton. A token ID is marked valid if and only if &lt;strong&gt;every character in the token's byte string represents a valid path in the grammar state machine&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Step 3: Runtime Logit Masking (The Mathematical Filter)
&lt;/h2&gt;

&lt;p&gt;Once the engine determines the subset of valid tokens $V_{\text{valid}} \subseteq V$ for the current state $S_t$, it constructs a &lt;strong&gt;Logit Mask Vector&lt;/strong&gt; $M \in \mathbb{R}^{|V|}$:&lt;/p&gt;

&lt;p&gt;$$M_i = \begin{cases} 0 &amp;amp; \text{if } i \in V_{\text{valid}} \ -\infty &amp;amp; \text{if } i \notin V_{\text{valid}} \end{cases}$$&lt;/p&gt;

&lt;p&gt;This mask is added directly to the raw logits vector $z$ output by the transformer forward pass before Softmax is computed:&lt;/p&gt;

&lt;p&gt;$$\tilde{z} = z + M$$&lt;/p&gt;

&lt;p&gt;$$\tilde{z}&lt;em&gt;i = \begin{cases} z_i &amp;amp; \text{if } i \in V&lt;/em&gt;{\text{valid}} \ -\infty &amp;amp; \text{if } i \notin V_{\text{valid}} \end{cases}$$&lt;/p&gt;

&lt;p&gt;Now, let us observe what happens when we calculate the Softmax probability for an invalid token $k \notin V_{\text{valid}}$:&lt;/p&gt;

&lt;p&gt;$$P(w_k) = \frac{e^{\tilde{z}&lt;em&gt;k}}{\sum&lt;/em&gt;{j=1}^{|V|} e^{\tilde{z}&lt;em&gt;j}} = \frac{e^{-\infty}}{\sum&lt;/em&gt;{j=1}^{|V|} e^{\tilde{z}&lt;em&gt;j}} = \frac{0}{\sum&lt;/em&gt;{j=1}^{|V|} e^{\tilde{z}_j}} = 0$$&lt;/p&gt;

&lt;p&gt;Because $e^{-\infty} = 0$, the probability of every invalid token becomes mathematically zero. &lt;/p&gt;

&lt;p&gt;When the sampler selects the next token, it is physically impossible to choose anything that violates the JSON schema. The model's learned knowledge and attention weights decide &lt;em&gt;which&lt;/em&gt; valid token to pick, but the grammar defines the sandbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Transformer Forward Pass ] ──► Raw Logits: [ 4.2,  1.1,  8.7,  -0.5, ... ] (128k items)
                                                    │
[ Grammar State + Vocab Trie ] ─► Mask Vector: [ 0.0, -inf,  0.0,  -inf, ... ]
                                                    │
                                                    ▼
                                Masked Logits: [ 4.2, -inf,  8.7,  -inf, ... ]
                                                    │
                                                    ▼
                                           [ Softmax Sampling ]
                                                    │
                                                    ▼
                                         Guaranteed Valid Token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Step 4: Deterministic Jump Decoding (Token Fast-Forwarding)
&lt;/h2&gt;

&lt;p&gt;Consider what happens when the model generates the key &lt;code&gt;"is_admin"&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;According to our schema, the key name is fixed. The colon &lt;code&gt;:&lt;/code&gt; is fixed. If the field is a boolean, the only allowed tokens are &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why execute heavy matrix multiplications across 70 billion parameters on an expensive GPU just to predict the characters &lt;code&gt;": "&lt;/code&gt; or the closing quote of a known key?&lt;/p&gt;

&lt;p&gt;Modern constrained decoding engines implement &lt;strong&gt;Jump Decoding&lt;/strong&gt; (also called Token Infill or Speculative Schema Fast-Forwarding):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At state $S_t$, the engine evaluates the set of valid next tokens $V_{\text{valid}}$.&lt;/li&gt;
&lt;li&gt;If $|V_{\text{valid}}| = 1$ (there is exactly one legal token), or if the schema specifies a literal string constant, the engine skips the transformer forward pass completely.&lt;/li&gt;
&lt;li&gt;It directly appends the known token to the output sequence, updates the KV cache, advances the grammar state machine, and continues to the next decision point.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Output: { "status": "active" }
          ▲       ▲▲        ▲▲
          │       ││        ││
Forward Pass?   NO (Fixed)  NO (Schema Key)  YES (Value Choice)  NO (Closing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In structured extraction tasks where 40% to 60% of the output tokens are predictable schema scaffolding (keys, brackets, whitespace, punctuation), jump decoding can increase overall generation throughput by &lt;strong&gt;2x to 3x&lt;/strong&gt; compared to unconstrained decoding.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Engineering Bottleneck: Masking Latency vs GPU Throughput
&lt;/h2&gt;

&lt;p&gt;While logit masking sounds simple in theory, naive implementations create severe inference bottlenecks.&lt;/p&gt;

&lt;p&gt;Consider high-throughput serving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A batch of 64 requests generating tokens on an NVIDIA H100.&lt;/li&gt;
&lt;li&gt;Vocabulary size: 128,000 tokens.&lt;/li&gt;
&lt;li&gt;Each decode step takes ~8ms on GPU.&lt;/li&gt;
&lt;li&gt;If evaluating the PDA and updating 128,000 logits in Python on the CPU takes 10ms per step, the GPU spends more time waiting for the CPU mask than doing tensor arithmetic!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Modern Engines Solve This (vLLM xgrammar &amp;amp; SGLang):
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-allocated Bitset Lookup Tables&lt;/strong&gt;: Rather than computing masks on the fly, static states (such as keywords, boolean branches, and punctuation) are precompiled into bitsets where each bit corresponds to a token ID in $V$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom CUDA Logit-Masking Kernels&lt;/strong&gt;: The bitset is copied to GPU memory once. A dedicated CUDA/Triton kernel applies the $-\infty$ mask in parallel across all 128k logits in less than 50 microseconds (0.05ms).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive Token Trie Pruning&lt;/strong&gt;: High-frequency prefixes are cached in L1/L2 CPU cache, minimizing pointer chasing during grammar transitions.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Practical Gotchas Developers Must Know
&lt;/h2&gt;

&lt;p&gt;Even though structured outputs guarantee syntax validity, they introduce real engineering trade-offs:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Model Distribution Distortion
&lt;/h3&gt;

&lt;p&gt;When you force an LLM to follow a strict schema, you are truncating its natural probability distribution. If your schema forces the model to output a specific field order that conflicts with how it reasons (for example, asking for &lt;code&gt;"final_answer"&lt;/code&gt; before &lt;code&gt;"explanation"&lt;/code&gt;), the model cannot perform step-by-step reasoning. Always put reasoning or thought fields &lt;em&gt;before&lt;/em&gt; categorical conclusions in your schemas.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Unbounded String Trap
&lt;/h3&gt;

&lt;p&gt;A schema with &lt;code&gt;{"type": "string"}&lt;/code&gt; allows any sequence of characters inside quotes. The grammar engine will ensure the string opens and closes with &lt;code&gt;"&lt;/code&gt;, but it cannot prevent the model from generating 4,000 tokens of repetitive text inside that string. Always use length constraints, enums, or regex patterns if you need bounded string outputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Schema Compilation Overhead
&lt;/h3&gt;

&lt;p&gt;Compiling complex nested schemas with hundreds of union types (&lt;code&gt;anyOf&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;) into a Pushdown Automaton can take 50ms to 500ms on the first request. Production systems solve this by precompiling and warming schema state machines at server startup rather than compiling per request.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary: The 4-Step Mental Model
&lt;/h2&gt;

&lt;p&gt;Whenever you use Structured Outputs in production, remember what is really happening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schema to PDA&lt;/strong&gt;: Your JSON schema is compiled into a Pushdown Automaton state machine with an explicit stack for tracking object/array nesting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vocab Trie Traversal&lt;/strong&gt;: The tokenizer's vocabulary is matched against the automaton to find the exact subset of valid token IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logit Masking&lt;/strong&gt;: Invalid token positions are overwritten with $-\infty$ before Softmax, reducing their sampling probability to exactly 0%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jump Decoding&lt;/strong&gt;: Deterministic schema scaffolding (keys, colons, brackets) bypasses GPU computation entirely and injects directly into the KV cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guaranteed JSON from LLMs is not magic prompt engineering; it is deterministic formal language theory controlling probabilistic token sampling.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>performance</category>
    </item>
    <item>
      <title>What Actually Happens During Speculative Decoding in LLMs</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Sun, 04 Oct 2026 13:09:36 +0000</pubDate>
      <link>https://dev.to/syed_anzar/what-actually-happens-during-speculative-decoding-in-llms-57a7</link>
      <guid>https://dev.to/syed_anzar/what-actually-happens-during-speculative-decoding-in-llms-57a7</guid>
      <description>&lt;h1&gt;
  
  
  What Actually Happens During Speculative Decoding in LLMs
&lt;/h1&gt;

&lt;p&gt;Autoregressive language model generation is notoriously slow. When you run a 70-billion parameter model on an enterprise GPU, you might get 20 to 30 tokens per second.&lt;/p&gt;

&lt;p&gt;The intuitive assumption is that the GPU compute cores are sweating under massive matrix multiplications. But if you profile the GPU during single-batch generation, you find the exact opposite: &lt;strong&gt;the GPU's compute cores are sitting 95% idle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The true bottleneck is memory bandwidth. Speculative decoding is an inference optimization that exploits this hardware reality to double or triple token generation speed with &lt;strong&gt;zero loss in mathematical output quality&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is what actually happens inside the GPU, the transformer layers, and the sampling math during speculative decoding.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Real Bottleneck: Memory Bandwidth vs Arithmetic Intensity
&lt;/h2&gt;

&lt;p&gt;To understand why speculative decoding works, you first have to look at the GPU memory bus.&lt;/p&gt;

&lt;p&gt;During autoregressive generation (decoding phase), an LLM generates tokens sequentially: one token per forward pass. To generate a single token for a batch size of 1:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The GPU must load every single weight parameter from High Bandwidth Memory (HBM / VRAM) into the on-chip SRAM / registers.&lt;/li&gt;
&lt;li&gt;It performs a vector-matrix multiplication between the single input token embedding and the model weights.&lt;/li&gt;
&lt;li&gt;It writes the updated Key-Value (KV) cache entry back to VRAM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let us do the concrete math for a &lt;strong&gt;70B parameter model in FP16&lt;/strong&gt; running on an &lt;strong&gt;Nvidia H100 GPU&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model size in VRAM:&lt;/strong&gt; 70 billion parameters * 2 bytes = 140 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H100 Memory Bandwidth:&lt;/strong&gt; ~3,350 GB/s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H100 Tensor Core Compute:&lt;/strong&gt; ~1,979 TFLOPs (FP16).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Time spent streaming weights:
&lt;/h3&gt;

&lt;p&gt;$$\text{Time}_{\text{memory}} = \frac{140\text{ GB}}{3,350\text{ GB/s}} \approx 41.8\text{ ms}$$&lt;/p&gt;

&lt;h3&gt;
  
  
  Time spent computing math:
&lt;/h3&gt;

&lt;p&gt;Generating 1 token requires roughly $2 \times 70 \times 10^9 = 1.4 \times 10^{11}$ FLOPs.&lt;br&gt;
$$\text{Time}_{\text{compute}} = \frac{1.4 \times 10^{11}\text{ FLOPs}}{1,979 \times 10^{12}\text{ FLOPs/s}} \approx 0.07\text{ ms}$$&lt;/p&gt;

&lt;p&gt;The ratio is brutal: the GPU spends &lt;strong&gt;41.8 ms loading weights from memory&lt;/strong&gt; and only &lt;strong&gt;0.07 ms doing arithmetic&lt;/strong&gt;. The arithmetic intensity (FLOPs per byte transferred) is nearly 1, far below the GPU's saturation threshold (which is around 150 FLOPs/byte).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional Autoregressive Decoding (Batch Size = 1):
[ Load 140 GB Weights ] ---&amp;gt; ( Compute 1 Token: 0.07 ms ) ===&amp;gt; Takes 41.8 ms
[ Load 140 GB Weights ] ---&amp;gt; ( Compute 1 Token: 0.07 ms ) ===&amp;gt; Takes 41.8 ms
[ Load 140 GB Weights ] ---&amp;gt; ( Compute 1 Token: 0.07 ms ) ===&amp;gt; Takes 41.8 ms
Result: 3 forward passes = ~125 ms for 3 tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The Core Insight: Parallel Verification is "Free"
&lt;/h2&gt;

&lt;p&gt;What happens if, instead of 1 token, you feed &lt;strong&gt;5 candidate tokens&lt;/strong&gt; into the 70B target model simultaneously?&lt;/p&gt;

&lt;p&gt;The model still loads the 140 GB weights from VRAM exactly once (41.8 ms). But now the Tensor Cores multiply a $[5 \times d]$ matrix against the weights instead of a $[1 \times d]$ vector. &lt;/p&gt;

&lt;p&gt;The compute time rises from 0.07 ms to 0.35 ms, but both numbers are dwarfed by the 41.8 ms memory transfer time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Evaluating 1 Token:  41.8 ms (memory) + 0.07 ms (compute) = 41.87 ms
Evaluating 5 Tokens: 41.8 ms (memory) + 0.35 ms (compute) = 42.15 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verifying 5 tokens in parallel takes virtually the exact same wall-clock time as generating 1 token.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Speculative decoding exploits this asymmetry by splitting generation into two distinct roles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A small, lightweight &lt;strong&gt;Draft Model&lt;/strong&gt; guesses $K$ tokens sequentially at high speed.&lt;/li&gt;
&lt;li&gt;The heavy &lt;strong&gt;Target Model&lt;/strong&gt; verifies all $K$ tokens simultaneously in one parallel forward pass.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. The Speculative Decoding Execution Loop
&lt;/h2&gt;

&lt;p&gt;Here is the exact step-by-step pipeline executed on every speculative decoding iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  +-----------------------------------+
                  |  Prompt / Current Prefix Sequence  |
                  +-----------------+-----------------+
                                    |
                                    v
                  +-----------------------------------+
                  |      Draft Model (e.g. 1B)        |
                  |  Generates K tokens sequentially  |
                  |  Tokens: [x1, x2, x3, x4, x5]     |
                  +-----------------+-----------------+
                                    |
                                    v
                  +-----------------------------------+
                  |      Target Model (e.g. 70B)      |
                  |  Parallel Forward Pass on prefix  |
                  |  + all K speculative tokens       |
                  +-----------------+-----------------+
                                    |
                                    v
                  +-----------------------------------+
                  |    Speculative Rejection Sampler  |
                  |  Accepts x1, x2, x3               |
                  |  Rejects x4                       |
                  |  Samples corrected replacement x4'|
                  |  Discards x5                      |
                  +-----------------+-----------------+
                                    |
                                    v
                  +-----------------------------------+
                  |  Append Accepted Tokens to Prefix |
                  |  Total yield: 4 tokens in 1 step! |
                  +-------------------+---------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: The Draft Phase
&lt;/h3&gt;

&lt;p&gt;A small model (such as a 1B parameter model paired with a 70B target) generates $K$ candidate tokens $(\hat{x}_1, \hat{x}_2, \dots, \hat{x}_K)$ autoregressively. &lt;/p&gt;

&lt;p&gt;Because the draft model is tiny (only ~2 GB in FP16), streaming its weights takes under 0.6 ms per token. Generating 5 draft tokens takes roughly $5 \times 0.6\text{ ms} = 3.0\text{ ms}$.&lt;/p&gt;

&lt;p&gt;For each generated token, the draft model records its predicted probability distribution $q(x)$.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: The Target Verification Phase
&lt;/h3&gt;

&lt;p&gt;All $K$ draft tokens are appended to the input sequence and evaluated by the 70B target model in &lt;strong&gt;one single forward pass&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because the attention mask allows each token at position $i$ to attend to all prior tokens, the target model produces the true output probability distributions $p_1(x), p_2(x), \dots, p_K(x)$ for all $K$ positions simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Speculative Sampling (Rejection &amp;amp; Correction)
&lt;/h3&gt;

&lt;p&gt;We iterate through the draft tokens from $i = 1$ to $K$:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For token $\hat{x}_i$, compare target probability $p(\hat{x}_i)$ against draft probability $q(\hat{x}_i)$.&lt;/li&gt;
&lt;li&gt;Draw a uniform random number $r \sim U[0, 1]$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance Condition:&lt;/strong&gt; If $r \le \min\left(1, \frac{p(\hat{x}_i)}{q(\hat{x}_i)}\right)$, accept token $\hat{x}_i$ and advance to $i+1$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejection Condition:&lt;/strong&gt; If the condition fails at position $j$, reject $\hat{x}&lt;em&gt;j$. Sample a replacement token from the residual distribution:
$$p'(x) = \frac{\max(0, p(x) - q(x))}{\sum&lt;/em&gt;{y} \max(0, p(y) - q(y))}$$
Immediately discard all subsequent draft tokens $(\hat{x}_{j+1}, \dots, \hat{x}_K)$, and conclude this iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bonus Token:&lt;/strong&gt; If all $K$ tokens are accepted, sample one extra $(K+1)$-th token directly from the target model's final logits $p_{K+1}(x)$.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. The Python Implementation
&lt;/h2&gt;

&lt;p&gt;Here is a clean, dependency-free reference implementation of Leviathan &amp;amp; Chen's Speculative Sampling algorithm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;speculative_sample_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft_probs_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_probs_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draft_tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    draft_probs_list: list of K numpy arrays, each shape (vocab_size,) representing q(x)
    target_probs_list: list of K+1 numpy arrays, each shape (vocab_size,) representing p(x)
    draft_tokens: list of K integers drafted by the small model

    Returns: list of accepted and sampled tokens
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;accepted_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;draft_tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;q_prob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;draft_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;p_prob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# Calculate acceptance probability
&lt;/span&gt;        &lt;span class="n"&gt;acceptance_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&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="n"&gt;p_prob&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_prob&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;acceptance_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Token accepted
&lt;/span&gt;            &lt;span class="n"&gt;accepted_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Token rejected: sample from modified residual distribution
&lt;/span&gt;            &lt;span class="n"&gt;residual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;draft_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;residual_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;residual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;residual_sum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;p_prime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;residual&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;residual_sum&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;p_prime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="n"&gt;replacement_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_prime&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p_prime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;accepted_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;replacement_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Discard all remaining draft tokens
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;accepted_tokens&lt;/span&gt;

    &lt;span class="c1"&gt;# All K tokens were accepted! Sample bonus (K+1)-th token directly from target
&lt;/span&gt;    &lt;span class="n"&gt;bonus_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; 
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target_probs_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;accepted_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bonus_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;accepted_tokens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Why the Output Distribution is 100% Lossless
&lt;/h2&gt;

&lt;p&gt;A common misconception among developers is that speculative decoding is an approximation or lossy distillation (like quantization or pruning).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is mathematically exact.&lt;/strong&gt; The probability of generating any sequence under speculative decoding is identical to generating directly from the large target model.&lt;/p&gt;

&lt;p&gt;Here is the simple algebraic proof:&lt;/p&gt;

&lt;p&gt;Let $x$ be the token candidate at position $i$. What is the total probability that token $x$ is emitted?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Probability $x$ was drafted and accepted:&lt;/strong&gt;&lt;br&gt;
$$P(\text{drafted } x) \times P(\text{accepted} \mid x) = q(x) \times \min\left(1, \frac{p(x)}{q(x)}\right) = \min(p(x), q(x))$$&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Probability another token was rejected and $x$ was sampled from $p'(x)$:&lt;/strong&gt;&lt;br&gt;
The total rejection probability across all possible tokens is:&lt;br&gt;
$$1 - \alpha = 1 - \sum_{y} \min(p(y), q(y)) = \sum_{y} (p(y) - \min(p(y), q(y))) = \sum_{y} \max(0, p(y) - q(y))$$&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When rejection happens, the probability of choosing $x$ from the normalized residual is:&lt;br&gt;
   $$p'(x) = \frac{\max(0, p(x) - q(x))}{1 - \alpha}$$&lt;/p&gt;

&lt;p&gt;Therefore, the joint probability of rejection and choosing $x$ is:&lt;br&gt;
   $$(1 - \alpha) \times \frac{\max(0, p(x) - q(x))}{1 - \alpha} = \max(0, p(x) - q(x)) = p(x) - \min(p(x), q(x))$$&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Total Probability:&lt;/strong&gt;
$$\text{Total } P(x) = \min(p(x), q(x)) + [p(x) - \min(p(x), q(x))] = p(x)$$&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The draft distribution $q(x)$ completely cancels out. Whether the draft model has 50% accuracy or 90% accuracy affects only &lt;strong&gt;speed&lt;/strong&gt;, never &lt;strong&gt;correctness&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Modern Variants: Beyond Standalone Draft Models
&lt;/h2&gt;

&lt;p&gt;While standalone draft models (like Llama-3-8B drafting for Llama-3-70B) work well, the open-source community and inference engines (vLLM, SGLang, llama.cpp) have developed even faster variations:&lt;/p&gt;
&lt;h3&gt;
  
  
  A. Medusa (Multi-Head Self-Speculative Decoding)
&lt;/h3&gt;

&lt;p&gt;Instead of maintaining an entire second model in VRAM, Medusa attaches multiple lightweight feed-forward decoding heads to the target model's final hidden state. Head 1 predicts token $t+1$, Head 2 predicts token $t+2$, Head 3 predicts token $t+3$. This eliminates the draft model footprint entirely.&lt;/p&gt;
&lt;h3&gt;
  
  
  B. EAGLE (Extensible Autoregressive Generation)
&lt;/h3&gt;

&lt;p&gt;Standard draft models operate on text tokens, losing the rich contextual embeddings computed by the target model. EAGLE passes the top-layer feature vectors of the target model into the draft head. This boosts token acceptance rates from ~60% to over 80-85%.&lt;/p&gt;
&lt;h3&gt;
  
  
  C. Tree-Based Attention (SpecInfer)
&lt;/h3&gt;

&lt;p&gt;Linear drafting guesses a single path $[x_1, x_2, x_3]$. If $x_1$ is rejected, everything else is wasted. Tree attention constructs a tree of multiple speculative branches (e.g. top-2 candidates at each depth). The target model verifies all candidate branches in parallel using a custom 2D attention mask, guaranteeing a higher average accepted token yield per step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Linear Speculation (Fragile):
[Root] -&amp;gt; x1 -&amp;gt; x2 -&amp;gt; x3 (If x1 fails, x2 and x3 are dead)

Tree Attention Speculation (Resilient):
               +--&amp;gt; x1a --&amp;gt; x2a
[Root] --------|
               +--&amp;gt; x1b --&amp;gt; x2b
(If x1a fails, x1b might still be accepted!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. When Speculative Decoding Fails (Production Gotchas)
&lt;/h2&gt;

&lt;p&gt;Speculative decoding is not a magic bullet for every deployment. Here are the 4 scenarios where it actually hurts performance:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. High Batch Sizes (Compute-Bound Saturation)
&lt;/h3&gt;

&lt;p&gt;Speculative decoding relies on spare GPU compute. When serving hundreds of concurrent requests (batch size 32, 64, or 128), your GPU is already 100% compute-bound. Adding draft forward passes creates compute contention and reduces overall throughput. Speculative decoding shines primarily at batch sizes 1 to 8 (low-latency streaming).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. High Temperature and Creative Entropy
&lt;/h3&gt;

&lt;p&gt;When sampling with high temperature ($T &amp;gt; 1.0$) on highly creative tasks, the output entropy is high. The overlap $\alpha = \sum \min(p, q)$ shrinks dramatically, causing frequent early rejections. Speculative decoding works best on structured output, code generation, and deterministic reasoning ($T \le 0.7$).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Tokenizer and Vocabulary Mismatches
&lt;/h3&gt;

&lt;p&gt;The draft model and target model must share the exact same tokenizer vocabulary and special token mappings. If token IDs do not map to identical string representations, the logits cannot be compared directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. VRAM Overhead on Edge Devices
&lt;/h3&gt;

&lt;p&gt;Running a draft model requires reserving additional GPU VRAM for its weights and its own separate KV cache. On memory-constrained GPUs (e.g. 16 GB or 24 GB consumer cards), this extra memory might push the target model into slower quantized formats or offload buffers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary &amp;amp; Mental Model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; Single-token decoding is memory-bandwidth bound. 99% of GPU latency is spent streaming weights from VRAM to compute cores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Workaround:&lt;/strong&gt; Evaluating $K$ tokens in parallel takes the same memory bandwidth time as evaluating 1 token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Execution:&lt;/strong&gt; A tiny draft model quickly proposes $K$ tokens. The target model verifies all $K$ tokens in a single parallel forward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Guarantee:&lt;/strong&gt; Leviathan's speculative sampling algorithm mathematically recovers the exact target model probability distribution with zero quality loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Sweet Spot:&lt;/strong&gt; Low batch sizes, low temperature, deterministic coding, and agent tool-calling loops.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>Python 3.13 Free-Threading: What Actually Happens When the GIL Is Removed</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Sun, 04 Oct 2026 06:40:13 +0000</pubDate>
      <link>https://dev.to/syed_anzar/python-313-free-threading-what-actually-happens-when-the-gil-is-removed-1on2</link>
      <guid>https://dev.to/syed_anzar/python-313-free-threading-what-actually-happens-when-the-gil-is-removed-1on2</guid>
      <description>&lt;p&gt;For over thirty years, every Python developer learned the same hard rule: CPU-bound code in standard Python cannot scale across multiple CPU cores using &lt;code&gt;threading&lt;/code&gt;. The Global Interpreter Lock (GIL) forced every thread to take turns executing bytecode, serialization was inevitable, and multi-core speedups required &lt;code&gt;multiprocessing&lt;/code&gt; with heavy IPC serialization overhead.&lt;/p&gt;

&lt;p&gt;Starting in Python 3.13, CPython introduced an experimental free-threaded build (&lt;code&gt;python3.13t&lt;/code&gt; via PEP 703) where the GIL is completely disabled.&lt;/p&gt;

&lt;p&gt;Turning off the GIL was not just removing a single lock. The GIL served as the safety blanket protecting Python's memory layout, reference counters, and internal dicts from data races. Removing it required re-engineering the foundational memory architecture of CPython.&lt;/p&gt;

&lt;p&gt;Here is what actually happens under the hood when the GIL is removed.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Core Problem: Why the GIL Existed in the First Place
&lt;/h2&gt;

&lt;p&gt;Every Python object contains a header with two fundamental fields: a pointer to its type object (&lt;code&gt;ob_type&lt;/code&gt;) and a reference counter (&lt;code&gt;ob_refcnt&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In standard GIL builds, whenever you pass a variable or read a list element, CPython increments or decrements &lt;code&gt;ob_refcnt&lt;/code&gt; using simple non-atomic CPU instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Standard CPython non-atomic refcount macro&lt;/span&gt;
&lt;span class="cp"&gt;#define Py_INCREF(op) (((PyObject*)(op))-&amp;gt;ob_refcnt++)
#define Py_DECREF(op) \
    if (--((PyObject*)(op))-&amp;gt;ob_refcnt == 0) _Py_Dealloc((PyObject*)(op))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-atomic instructions (&lt;code&gt;++&lt;/code&gt; and &lt;code&gt;--&lt;/code&gt;) take multiple CPU cycles: load value into register, increment register, store back to memory. If two OS threads modify that counter simultaneously without synchronization, you hit race conditions, missed decrements, double frees, and memory corruption.&lt;/p&gt;

&lt;p&gt;The GIL avoided this by guaranteeing that only one thread touched &lt;code&gt;ob_refcnt&lt;/code&gt; at any given moment.&lt;/p&gt;

&lt;p&gt;Simply replacing every refcount operation with atomic assembly instructions (&lt;code&gt;LOCK XADD&lt;/code&gt; on x86) was tried in earlier experimental forks (like Larry Hastings' Gilectomy). The result: single-threaded Python code slowed down by 30% to 50% because atomic bus-locking instructions invalidate CPU L1/L2 caches and choke CPU pipelines.&lt;/p&gt;

&lt;p&gt;To make free-threading viable, PEP 703 implemented &lt;strong&gt;Biased Reference Counting (BRC)&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Biased Reference Counting (BRC): Fast-Path vs Shared-Path
&lt;/h2&gt;

&lt;p&gt;Biased Reference Counting is built on a simple observation: in real-world programs, most objects are created and manipulated exclusively by the single thread that allocated them.&lt;/p&gt;

&lt;p&gt;Instead of one global reference count, free-threaded CPython splits reference counting into two distinct paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local Refcount (&lt;code&gt;ob_ref_local&lt;/code&gt;)&lt;/strong&gt;: Managed exclusively by the thread that owns (created) the object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Refcount (&lt;code&gt;ob_ref_shared&lt;/code&gt;)&lt;/strong&gt;: Managed atomically by any other thread accessing the object.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------+
|                     Python Object Header                    |
+-------------------------------------------------------------+
|  ob_tid (Thread ID of Owner)                                |
|  ob_ref_local   (16-bit: Non-atomic fast updates by owner)  |
|  ob_ref_shared  (32-bit: Atomic atomic updates by peers)   |
|  ob_type        (Pointer to Type descriptor)                |
+-------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Fast Path (Owner Thread)
&lt;/h3&gt;

&lt;p&gt;When the owning thread increments or decrements an object's reference counter, it checks if &lt;code&gt;current_thread_id == object-&amp;gt;ob_tid&lt;/code&gt;. If true, it performs a plain, non-atomic addition to &lt;code&gt;ob_ref_local&lt;/code&gt;. This costs zero bus locks and keeps single-thread execution fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Slow Path (Foreign Threads)
&lt;/h3&gt;

&lt;p&gt;When another thread references that same object, it cannot touch &lt;code&gt;ob_ref_local&lt;/code&gt;. Instead, it issues an atomic operation on &lt;code&gt;ob_ref_shared&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merging and Deallocation
&lt;/h3&gt;

&lt;p&gt;When does the object actually get freed?&lt;br&gt;
The owning thread periodically merges &lt;code&gt;ob_ref_shared&lt;/code&gt; into &lt;code&gt;ob_ref_local&lt;/code&gt;. When &lt;code&gt;ob_ref_local + (ob_ref_shared &amp;gt;&amp;gt; 2) == 0&lt;/code&gt;, the owning thread triggers deallocation. If a non-owning thread drops the final reference while the owning thread is idle, an evaluation interrupt (&lt;code&gt;eval_breaker&lt;/code&gt;) signals the owning thread to process the deferred cleanup safely.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Immortal Objects: Stopping Cache-Line Bouncing
&lt;/h2&gt;

&lt;p&gt;Some objects are read constantly by every single running thread: &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;True&lt;/code&gt;, &lt;code&gt;False&lt;/code&gt;, small integers (&lt;code&gt;0&lt;/code&gt; to &lt;code&gt;256&lt;/code&gt;), built-in exceptions, type definitions (&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;str&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;), and top-level function code objects.&lt;/p&gt;

&lt;p&gt;If 16 parallel threads constantly run &lt;code&gt;Py_INCREF(Py_None)&lt;/code&gt; and &lt;code&gt;Py_DECREF(Py_None)&lt;/code&gt;, atomic cache-line invalidation would destroy multi-core performance. CPU cores would spend more time invalidating each other's L1 cache lines than executing code.&lt;/p&gt;

&lt;p&gt;CPython solves this with &lt;strong&gt;Immortalization&lt;/strong&gt; (PEP 683, adapted for PEP 703).&lt;/p&gt;

&lt;p&gt;In free-threaded builds, immortal objects have their refcount bitfield set to a special constant (&lt;code&gt;_Py_IMMORTAL_REFCNT&lt;/code&gt;). The refcount macros check this bitfield:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_Py_INCREF_STAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Py_IsImmortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// No-op: Zero memory writes, zero cache invalidation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Proceed to biased reference counting...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because immortal objects never modify memory during refcounting, thousands of threads can read &lt;code&gt;None&lt;/code&gt;, interned strings, or module definitions concurrently with zero memory contention.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Goodbye pymalloc, Hello mimalloc
&lt;/h2&gt;

&lt;p&gt;Standard CPython relies on &lt;code&gt;pymalloc&lt;/code&gt;, an allocator optimized for small Python objects under 512 bytes. However, &lt;code&gt;pymalloc&lt;/code&gt; is completely non-thread-safe and relies entirely on the GIL for synchronization.&lt;/p&gt;

&lt;p&gt;Rather than bolting locks onto &lt;code&gt;pymalloc&lt;/code&gt; (which would kill allocation speed), free-threaded Python 3.13 replaces it with Microsoft's &lt;strong&gt;mimalloc&lt;/strong&gt; allocator.&lt;/p&gt;

&lt;p&gt;Mimalloc brings critical architectural advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thread-Local Heaps&lt;/strong&gt;: Each thread maintains its own allocation arenas. Allocating a new dictionary or string requires zero global locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radix-Tree Page Lookups&lt;/strong&gt;: Enables the Garbage Collector to find and traverse all Python objects directly through heap metadata, eliminating the need to maintain global doubly-linked lists across all objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QSBR (Quiescent-State Based Reclamation)&lt;/strong&gt;: Prevents use-after-free bugs when memory pages are returned to the operating system while other threads are finishing up reads.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Per-Object Critical Sections: Replacing the Global Lock
&lt;/h2&gt;

&lt;p&gt;Without the GIL, what prevents two threads from mutating the same &lt;code&gt;list&lt;/code&gt; or &lt;code&gt;dict&lt;/code&gt; simultaneously and corrupting internal hash tables?&lt;/p&gt;

&lt;p&gt;CPython 3.13 introduces &lt;strong&gt;Per-Object Critical Sections&lt;/strong&gt; (&lt;code&gt;Py_BEGIN_CRITICAL_SECTION&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Instead of locking the entire runtime, Python locks only the specific container being modified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: Modifying a list in free-threaded CPython&lt;/span&gt;
&lt;span class="n"&gt;Py_BEGIN_CRITICAL_SECTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list_obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Exclusive lock on list_obj held here&lt;/span&gt;
&lt;span class="n"&gt;list_append_internal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Py_END_CRITICAL_SECTION&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automatic Deadlock Avoidance
&lt;/h3&gt;

&lt;p&gt;What if Thread A locks List 1 and wants List 2, while Thread B locks List 2 and wants List 1?&lt;/p&gt;

&lt;p&gt;CPython critical sections maintain a thread-local stack of active locks. If a thread attempts to acquire an object lock that is currently held by another thread, the runtime automatically &lt;strong&gt;suspends&lt;/strong&gt; all active critical sections held by the waiting thread, releases their locks, yields, and re-acquires them in a strict ordered hierarchy before continuing.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. What This Means for Python Developers
&lt;/h2&gt;

&lt;p&gt;Running Python 3.13 free-threaded (&lt;code&gt;python3.13t&lt;/code&gt;) delivers real CPU-bound concurrency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Free threading enabled: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_is_gil_enabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cpu_heavy_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# In Python 3.13t, this runs in true parallel across all 8 CPU cores
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&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="n"&gt;cpu_heavy_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;True Parallel Threads&lt;/strong&gt;: CPU-bound tasks in standard Python threads now scale linearly with CPU core counts without &lt;code&gt;multiprocessing&lt;/code&gt; IPC overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thread Safety Still Matters&lt;/strong&gt;: Built-in containers are safe from memory crashes due to internal critical sections, but race conditions in application logic still require explicit locks (&lt;code&gt;threading.Lock&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C-Extensions Need Adaptation&lt;/strong&gt;: C extensions must be compiled specifically for the free-threaded ABI (&lt;code&gt;python3.13t&lt;/code&gt;) to verify they don't rely on the legacy GIL for internal struct safety.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Python's transition to free-threading is one of the most substantial engineering feats in modern language runtimes, unlocking decades of untapped multi-core performance while maintaining the syntax and ergonomics developers rely on.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>performance</category>
      <category>systems</category>
    </item>
    <item>
      <title>Why LLMs Run Out of VRAM: KV Cache Fragmentation and How PagedAttention Fixes It</title>
      <dc:creator>Syed Anzar</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:34:22 +0000</pubDate>
      <link>https://dev.to/syed_anzar/why-llms-run-out-of-vram-kv-cache-fragmentation-and-how-pagedattention-fixes-it-fle</link>
      <guid>https://dev.to/syed_anzar/why-llms-run-out-of-vram-kv-cache-fragmentation-and-how-pagedattention-fixes-it-fle</guid>
      <description>&lt;p&gt;You deploy a 13-billion parameter model quantized to 4-bit weights. The static model weights consume roughly 7.5 GB of VRAM. You put it on an NVIDIA RTX 4090 with 24 GB of memory, confident you have more than 16 GB of headroom for traffic.&lt;/p&gt;

&lt;p&gt;Then you run a batch of 8 concurrent requests with 4,000-token context windows.&lt;/p&gt;

&lt;p&gt;Within seconds, your server throws &lt;code&gt;torch.cuda.OutOfMemoryError: CUDA out of memory&lt;/code&gt; or your throughput collapses from 80 tokens per second to single digits.&lt;/p&gt;

&lt;p&gt;Where did those 16 gigabytes go?&lt;/p&gt;

&lt;p&gt;Most developers assume GPU memory during inference is dominated by model weights. In reality, once an LLM starts serving concurrent users, &lt;strong&gt;the Key-Value (KV) Cache quickly swallows more memory than the model itself&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Even worse: in naive serving setups, &lt;strong&gt;up to 60% to 80% of that VRAM is pure waste&lt;/strong&gt;, locked away by memory fragmentation.&lt;/p&gt;

&lt;p&gt;Here is what is actually happening inside GPU memory during inference, why naive allocators fail, and how an idea borrowed from 1960s operating systems (PagedAttention) fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why Transformers Need a KV Cache
&lt;/h2&gt;

&lt;p&gt;To understand why memory explodes, look at how autoregressive token generation works.&lt;/p&gt;

&lt;p&gt;Generating text is an iterative loop. When generating token $t$:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The token is embedded and passed through $N$ transformer layers.&lt;/li&gt;
&lt;li&gt;In each self-attention layer, the model computes Query ($Q$), Key ($K$), and Value ($V$) projections.&lt;/li&gt;
&lt;li&gt;Attention is calculated: 
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$&lt;/li&gt;
&lt;li&gt;To compute the attention of the new token $t$ against all previous tokens $(1 \dots t-1)$, the model needs the $K$ and $V$ vectors of &lt;strong&gt;every previous token&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1:  [Prompt: "The", "quick", "brown"]  -&amp;gt; Generates "fox"
Step 2:  ["The", "quick", "brown", "fox"]   -&amp;gt; Generates "jumps"
Step 3:  ["The", "quick", "brown", "fox", "jumps"] -&amp;gt; Generates "over"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without caching, generating token 1,000 would require recomputing the Key and Value matrices for all 999 previous tokens across every single transformer layer. That would turn inference complexity into $O(n^2)$ compute.&lt;/p&gt;

&lt;p&gt;To avoid this quadratic recalculation, inference engines store the $K$ and $V$ tensors of all previous tokens in GPU High-Bandwidth Memory (HBM). On every step, we only compute $Q$, $K$, and $V$ for the &lt;em&gt;single new token&lt;/em&gt;, append its $K$ and $V$ to the cache, and calculate attention against the stored history.&lt;/p&gt;

&lt;p&gt;Compute drops to $O(1)$ per step. But your memory requirement becomes dynamic and strictly increasing.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Concrete Math of KV Cache Growth
&lt;/h2&gt;

&lt;p&gt;How big is a KV cache in physical memory?&lt;/p&gt;

&lt;p&gt;For every generated token, at every transformer layer, we must store two vectors ($K$ and $V$).&lt;/p&gt;

&lt;p&gt;The exact formula for KV cache memory per token is:&lt;/p&gt;

&lt;p&gt;$$\text{Bytes per Token} = 2 \times n_{\text{layers}} \times n_{\text{kv_heads}} \times d_{\text{head}} \times b$$&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$2$ accounts for both the Key and Value tensors&lt;/li&gt;
&lt;li&gt;$n_{\text{layers}}$ is the number of transformer layers&lt;/li&gt;
&lt;li&gt;$n_{\text{kv_heads}}$ is the number of Key-Value attention heads&lt;/li&gt;
&lt;li&gt;$d_{\text{head}}$ is the hidden dimension per head&lt;/li&gt;
&lt;li&gt;$b$ is the precision in bytes (2 bytes for FP16/BF16, 1 byte for FP8)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example 1: Multi-Head Attention (Llama-2-13B)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Layers ($n_{\text{layers}}$): 40&lt;/li&gt;
&lt;li&gt;KV Heads ($n_{\text{kv_heads}}$): 40 (standard Multi-Head Attention)&lt;/li&gt;
&lt;li&gt;Head Dimension ($d_{\text{head}}$): 128&lt;/li&gt;
&lt;li&gt;Precision: 16-bit float (2 bytes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\text{Bytes per Token} = 2 \times 40 \times 40 \times 128 \times 2 = 819,200 \text{ bytes} \approx 800\text{ KB per token}$$&lt;/p&gt;

&lt;p&gt;For a single request processing a 4,096-token context:&lt;br&gt;
$$4,096 \times 800\text{ KB} \approx 3.2\text{ GB per sequence}$$&lt;/p&gt;

&lt;p&gt;If you serve &lt;strong&gt;8 concurrent users&lt;/strong&gt; at 4k context:&lt;br&gt;
$$8 \times 3.2\text{ GB} = \mathbf{25.6\text{ GB of VRAM}}$$&lt;/p&gt;

&lt;p&gt;The KV cache alone exceeds the total VRAM of an RTX 4090 before you even account for model weights or CUDA overhead.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 2: Grouped-Query Attention (Llama-3-8B)
&lt;/h3&gt;

&lt;p&gt;Modern architectures use Grouped-Query Attention (GQA) where multiple query heads share a single KV head.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layers: 32&lt;/li&gt;
&lt;li&gt;KV Heads: 8 (sharing across 32 query heads)&lt;/li&gt;
&lt;li&gt;Head Dimension: 128&lt;/li&gt;
&lt;li&gt;Precision: 16-bit float (2 bytes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\text{Bytes per Token} = 2 \times 32 \times 8 \times 128 \times 2 = 131,072 \text{ bytes} = 128\text{ KB per token}$$&lt;/p&gt;

&lt;p&gt;At 4,096 tokens, one request takes &lt;strong&gt;512 MB&lt;/strong&gt;. A batch of 32 requests takes &lt;strong&gt;16 GB&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. The Real Culprit: Memory Fragmentation
&lt;/h2&gt;

&lt;p&gt;If a request only needs 512 MB at 4k tokens, why did naive serving engines run out of memory with only a handful of short requests?&lt;/p&gt;

&lt;p&gt;The problem lies in how standard PyTorch memory allocators manage dynamic tensors.&lt;/p&gt;

&lt;p&gt;Standard attention kernels (like basic PyTorch MultiheadAttention or cuDNN) expect the Key and Value tensors of a sequence to be stored in &lt;strong&gt;contiguous virtual and physical memory&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Naive Allocator (Contiguous Slot per Sequence):
Request A: [Token 1][Token 2][Token 3] ... [Reserved Empty Space up to 4096]
Request B: [Token 1][Token 2] ............ [Reserved Empty Space up to 4096]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the inference engine cannot predict when a model will emit the &lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt; token, it faces three severe structural inefficiencies:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Internal Fragmentation (Static Max-Length Reservation)
&lt;/h3&gt;

&lt;p&gt;To prevent constant memory reallocation and costly CUDA memory copies as sequences grow, naive engines pre-allocate a contiguous memory buffer sized for &lt;code&gt;max_model_len&lt;/code&gt; (e.g., 4,096 tokens). &lt;/p&gt;

&lt;p&gt;If a user prompt only generates 350 tokens and terminates, the remaining 3,746 token slots (over 90% of the allocated buffer) sit completely unused, yet locked.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reservation Waste (Future Slots)
&lt;/h3&gt;

&lt;p&gt;Even if an engine attempts dynamic chunking, it must reserve memory for future tokens that have not arrived yet. That reserved memory cannot be assigned to other concurrent requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. External Fragmentation
&lt;/h3&gt;

&lt;p&gt;Requests finish at different times. As short requests finish and free their memory chunks, the GPU memory pool becomes a checkerboard of small, non-contiguous free memory holes. &lt;/p&gt;

&lt;p&gt;Even if you have 8 GB of total free VRAM, if the largest contiguous block is only 200 MB, a new incoming request requiring a 500 MB contiguous buffer will immediately crash with an Out of Memory error.&lt;/p&gt;

&lt;p&gt;In research published by UC Berkeley (Kwon et al., SOSP 2023), measurements showed that in traditional serving systems like FasterTransformer and Orca, &lt;strong&gt;only 20% to 40% of the allocated KV cache memory actually held useful token states&lt;/strong&gt;. The remaining 60% to 80% was lost to fragmentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. How PagedAttention Solves Fragmentation
&lt;/h2&gt;

&lt;p&gt;Operating systems solved the exact same problem for system RAM in the 1960s with &lt;strong&gt;Virtual Memory Paging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A process sees a continuous virtual address space ($0 \dots N$), but the OS kernel splits physical RAM into fixed-size 4 KB pages. A CPU Page Table maps each virtual page to any available non-contiguous physical memory frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PagedAttention&lt;/strong&gt; (the core engine powering vLLM, SGLang, and modern inference backends) applies this exact concept to GPU VRAM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logical KV Cache (Sequence 0):
[Block 0: Tokens 0-15] -&amp;gt; [Block 1: Tokens 16-31] -&amp;gt; [Block 2: Tokens 32-47]
           │                         │                         │
           ▼                         ▼                         ▼
Physical GPU Memory (Non-contiguous):
[Physical Block 84]       [Physical Block 12]       [Physical Block 903]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Architecture Works:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixed-Size KV Blocks&lt;/strong&gt;: The KV cache of a sequence is partitioned into fixed blocks of tokens (typically 16 or 32 tokens per block).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Physical Block Pool&lt;/strong&gt;: At startup, vLLM pre-allocates all available GPU memory into a pool of physical blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block Tables&lt;/strong&gt;: For each active sequence, the engine maintains a Block Table (analogous to an OS page table). The block table records the physical block ID where each logical block resides, along with how many slots in the latest block are filled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-Demand Allocation&lt;/strong&gt;: When a request starts, the engine allocates only one physical block (16 tokens). As the model generates tokens 1 through 15, it writes into the existing block. When it hits token 16, the engine grabs another physical block from the pool and records its address in the Block Table.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The PagedAttention CUDA Kernel
&lt;/h3&gt;

&lt;p&gt;Standard cuBLAS and FlashAttention kernels require contiguous memory pointers. &lt;/p&gt;

&lt;p&gt;PagedAttention replaces the attention operation with a custom GPU kernel. During the decoding step, the kernel reads the query token $Q_i$, looks up the sequence's Block Table, and dynamically fetches $K$ and $V$ vectors directly from scattered physical memory addresses across GPU HBM during the matrix multiplication loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal Fragmentation&lt;/strong&gt;: Limited strictly to the last uncompleted block of a sequence (less than 4% memory waste with block size 16).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External Fragmentation&lt;/strong&gt;: &lt;strong&gt;0%&lt;/strong&gt;. Any free block anywhere in memory can be assigned to any sequence immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effective Batch Size&lt;/strong&gt;: Increases by &lt;strong&gt;2x to 4x on the exact same hardware&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Superpowers of Paged Memory: Zero-Copy Forking
&lt;/h2&gt;

&lt;p&gt;Because memory is managed through indirection (block tables pointing to physical blocks), PagedAttention unlocks memory optimizations that are impossible with contiguous arrays.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Parallel Sampling &amp;amp; Tree Search
&lt;/h3&gt;

&lt;p&gt;Suppose you want the model to generate 4 candidate completions for the same prompt (e.g., beam search, voting, or speculative draft trees).&lt;/p&gt;

&lt;p&gt;In naive systems, you must duplicate the prompt's KV cache 4 times in memory.&lt;/p&gt;

&lt;p&gt;With PagedAttention, all 4 sequences point their block tables to the &lt;strong&gt;same physical prompt blocks&lt;/strong&gt;. The physical blocks are marked with a reference count of 4.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request Prompt (Tokens 0-31): Physical Block #7, Physical Block #14 (Ref Count = 2)

Branch A (Tokens 32-47) -&amp;gt; Writes to new Physical Block #89 (Private)
Branch B (Tokens 32-47) -&amp;gt; Writes to new Physical Block #102 (Private)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only when a branch generates its first new token does the engine allocate a new private block for that specific branch (&lt;strong&gt;Copy-on-Write&lt;/strong&gt;). Memory consumption for the prompt is reduced by 75%.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Automatic Prefix Caching
&lt;/h3&gt;

&lt;p&gt;In agentic workflows, system prompts, tool schemas, and multi-turn conversation history are reused constantly across requests.&lt;/p&gt;

&lt;p&gt;With prefix caching enabled, when a new request arrives containing a system prompt that matches a previously computed sequence, vLLM checks its hash table of physical blocks. If the tokens match, it skips the prefill compute phase entirely and simply maps the existing physical blocks into the new request's block table.&lt;/p&gt;

&lt;p&gt;First-token latency (Time-to-First-Token) drops from hundreds of milliseconds to near zero, and VRAM usage for the shared prefix drops to 0 additional bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Practical Tuning Guide for Production Inference
&lt;/h2&gt;

&lt;p&gt;If you are running self-hosted LLMs using vLLM, SGLang, or Ollama, here is how to tune these memory parameters for maximum throughput:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tune GPU Memory Utilization
&lt;/h3&gt;

&lt;p&gt;By default, vLLM sets &lt;code&gt;gpu_memory_utilization=0.90&lt;/code&gt;. This reserves 90% of total VRAM for model weights and the KV block pool, leaving 10% for PyTorch activation buffers and CUDA contexts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For dedicated serving nodes with fixed models, raise utilization to 0.95&lt;/span&gt;
vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.95 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 8192
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you experience random CUDA initialization crashes, lower it to &lt;code&gt;0.85&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Select the Right Block Size
&lt;/h3&gt;

&lt;p&gt;The default block size in vLLM is 16.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block size 16&lt;/strong&gt;: Lowest internal fragmentation. Best for workloads with short, unpredictable outputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block size 32&lt;/strong&gt;: Better GPU memory coalescing and slightly higher arithmetic throughput on high-end GPUs (A100/H100), with slightly higher tail waste.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="nt"&gt;--block-size&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Enable Prefix Caching for Multi-Turn Agents
&lt;/h3&gt;

&lt;p&gt;If you run coding assistants, customer support bots, or agent loops that share large system prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-prefix-caching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Enable FP8 KV Cache
&lt;/h3&gt;

&lt;p&gt;If you are memory-bound rather than compute-bound, you can quantize the KV cache from FP16 (2 bytes) to FP8 (1 byte). This cuts your KV memory usage in half with negligible perplexity degradation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve meta-llama/Llama-3.1-8B-Instruct &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kv-cache-dtype&lt;/span&gt; fp8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary: Contiguous vs Paged Attention
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Naive Contiguous Allocation&lt;/th&gt;
&lt;th&gt;PagedAttention (vLLM)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Layout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contiguous virtual &amp;amp; physical DRAM&lt;/td&gt;
&lt;td&gt;Non-contiguous fixed-size blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Waste&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60% – 80% (Internal &amp;amp; External fragmentation)&lt;/td&gt;
&lt;td&gt;&amp;lt; 4% (Last-block tail only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max Batch Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Constrained by peak pre-allocation&lt;/td&gt;
&lt;td&gt;Dynamic, maximizes hardware saturation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt Sharing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires full memory duplication&lt;/td&gt;
&lt;td&gt;Zero-copy pointer sharing (Copy-on-Write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prefix Caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex tensor slicing &amp;amp; copying&lt;/td&gt;
&lt;td&gt;Instant block hash lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The next time your local or production LLM runs out of VRAM, remember: the bottleneck is rarely just the model parameters. Understanding how the KV cache grows and how paging eliminates memory waste is the single most important lever for scaling LLM inference throughput.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>performance</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
