<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Engineer’s Notebook]]></title><description><![CDATA[A long-form notebook on backend engineering, Go, distributed systems, and cloud architecture.
Written while learning, building, and thinking deeply—one concept/system at a time.]]></description><link>https://logs.craftedbyvishal.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!Fb4C!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91c7b792-c94d-49e4-995f-85ca92e5a27d_512x512.png</url><title>The Engineer’s Notebook</title><link>https://logs.craftedbyvishal.dev</link></image><generator>Substack</generator><lastBuildDate>Sun, 28 Jun 2026 18:13:10 GMT</lastBuildDate><atom:link href="https://logs.craftedbyvishal.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Vishal Govind]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[vishalageek@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[vishalageek@substack.com]]></itunes:email><itunes:name><![CDATA[Vishal Govind]]></itunes:name></itunes:owner><itunes:author><![CDATA[Vishal Govind]]></itunes:author><googleplay:owner><![CDATA[vishalageek@substack.com]]></googleplay:owner><googleplay:email><![CDATA[vishalageek@substack.com]]></googleplay:email><googleplay:author><![CDATA[Vishal Govind]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[I Thought Connection Pooling Was About Caching. It's About Framing Protocols and Synchronization Knobs.]]></title><description><![CDATA[Why knowing when a response ends is only half the problem &#8212; the other half is controlling bursts, memory, and server load with semaphores and mutexes?]]></description><link>https://logs.craftedbyvishal.dev/p/i-thought-connection-pooling-was</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/i-thought-connection-pooling-was</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Sun, 10 May 2026 20:28:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nbOW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A while back I wrote about Go&#8217;s netpoller and the full lifecycle of a TCP connection. You can read it <a href="https://open.substack.com/pub/vishalageek/p/gos-netpoller-handles-millions-of?r=n6j9g&amp;utm_campaign=post&amp;utm_medium=web">here</a>. It ended with:</p><blockquote><p>While Go&#8217;s netpoller solves thread efficiency, pools address the underlying connection cost problem.</p></blockquote><p>That closing line sat with me for days. I understood the netpoller, the kernel queues, the handshake, pollDesc, epoll. But I had no concrete picture of what a pool actually is or how it decides when a connection is safe to reuse.</p><p>So I built one from scratch. Raw TCP, no frameworks, just <code>net.Dial</code>, <code>net.Listen</code>, and the same connection lifecycle I mapped in the previous article.</p><p>This is what I found.</p><h2>Connection Pooling Starts with a Server Decision</h2><p>The first thing that surprised me: connection pooling is not a client invention. It requires an explicit server choice.</p><p>A naive server closes after every response:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">conn, _ := listener.Accept()
handleRequest(conn)
conn.Close()  // connection dead
</code></pre></div><p>This is what I built first. Clean, simple, and expensive. Every new request pays the full cost again: TCP handshake (SYN, SYN-ACK, ACK), buffer allocation, pollDesc registration, the whole setup.</p><p>A pooling-aware server does the opposite. It stays in a loop:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">conn, _ := listener.Accept()
sc := bufio.NewScanner(conn)
for sc.Scan() {
    handleRequest(sc.Text())
    fmt.Fprintf(conn, "response\n")
}
// loop only exits when client disconnects or idle timeout fires
</code></pre></div><p>The <code>for sc.Scan()</code> is the deliberate decision. The server goroutine stays parked on the netpoller between requests, no OS thread consumed, connection held open. This is what gives the client something to reuse.</p><p>By not closing, the server lets the client leverage pooling. The benefit runs both ways: client avoids the handshake cost, server reuses its own goroutine instead of spawning new ones per connection.</p><h2>The Framing Contract</h2><p>The server staying in the loop creates a fundamental problem. How does it know where one request ends and the next begins?</p><p>The client writes:</p><pre><code><code>request1\nrequest2\nrequest3\n
</code></code></pre><p>The server&#8217;s <code>sc.Scan()</code> uses <code>\n</code> as the boundary. It reads until it hits the delimiter, then stops. Real databases use length-prefixed frames or special terminator bytes. Same idea, different encoding.</p><p>But there&#8217;s a second problem. How does the client know when the full response has arrived and it&#8217;s safe to return the connection to the pool?</p><p>Return the connection mid-response and the next caller gets a connection with leftover bytes in the recv buffer. Corrupted data.</p><p>How different systems solve this:</p><ul><li><p><strong>Postgres:</strong> sends <code>CommandComplete</code> + <code>ReadyForQuery</code> frames. <code>ReadyForQuery</code> means &#8220;full response delivered, connection is idle.&#8221;</p></li><li><p><strong>HTTP chunked:</strong> <code>0\r\n</code> terminating chunk signals &#8220;stream over.&#8221;</p></li><li><p><strong>HTTP with Content-Length:</strong> client reads exactly N bytes, done.</p></li></ul><p>Pooling and framing are inseparable. Without the sentinel, the pool can&#8217;t know when a connection is safe to reuse. This is why database drivers and HTTP clients always implement a framing protocol alongside the pool. They&#8217;re two halves of the same contract.</p><h2>Building the Simplest Pool</h2><p>I started with a buffered channel:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type ConnPool struct {
    pool chan net.Conn  // capacity = MaxConn
    Addr string
}

func (p *ConnPool) GetConn() (net.Conn, error) {
    select {
    case conn := &lt;-p.pool:
        return conn, nil
    default:
        return net.Dial("tcp", p.Addr)
    }
}

func (p *ConnPool) ReturnConn(c net.Conn) {
    select {
    case p.pool &lt;- c:
    default:
        c.Close()  // pool full, close to avoid fd leak
    }
}
</code></pre></div><p>The <code>default</code> case in both is key. In <code>GetConn</code>: pool empty, dial fresh. In <code>ReturnConn</code>: pool full, close the connection. Not drop, close. Dropping it leaks the fd and kernel socket struct.</p><p>A buffered channel is the synchronization primitive. No mutex needed, channels are already thread-safe.</p><p>I ran this with a pool size of 3 and created 5 connections. The first 3 went into the pool. The 4th and 5th hit the <code>default</code> case in <code>ReturnConn</code> and were closed immediately.</p><p>The proof of reuse: same ephemeral port.</p><pre><code><code>iter[0]: conn.LocalAddress(127.0.0.1:55645)
iter[1]: conn.LocalAddress(127.0.0.1:55645)  &#8592; same port
</code></code></pre><p>The kernel assigns one ephemeral port per TCP connection. Same port across two <code>GetConn</code> calls = same <code>net.Conn</code> returned from pool = zero new dials.</p><h2>The Overflow Demo</h2><p>I wanted to see what happens when more connections are created than the pool can hold.</p><p>Setup: pool size = 3, <code>MaxConn = 3</code>. I created 5 connections total. The first 3 should go into the pool. The last 2 should hit the <code>default</code> case in <code>ReturnConn</code> and be closed immediately.</p><p>The code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">pool := pool.New(":4040")

// grab 2 extra connections before warming pool
conn3, _ := pool.GetConn()
conn4, _ := pool.GetConn()

// warm up pool with 3 connections
conns := []net.Conn{}
for i := 0; i &lt; 3; i++ {
    conn, _ := pool.GetConn()
    conns = append(conns, conn)
}
for _, conn := range conns {
    pool.ReturnConn(conn)  // pool now full: 3 connections
}

// first loop: grab from pool, use, return
for i := 0; i &lt; 3; i++ {
    conn, _ := pool.GetConn()
    handleConn(i, conn, pool)  // i = 0, 1, 2
}

// second loop: same 3 connections reused
for i := 0; i &lt; 3; i++ {
    conn, _ := pool.GetConn()
    handleConn(i, conn, pool)  // i = 0, 1, 2 again
}

// overflow: these two hit pool full &#8594; closed
handleConn(3, conn3, pool)
handleConn(4, conn4, pool)
</code></pre></div><p>Each <code>handleConn</code> writes one line with the iteration number <code>i</code>, reads the echo response, calls <code>ReturnConn</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func handleConn(i int, conn net.Conn, pool *pool.ConnPool) {
    fmt.Printf("iter[%d]: conn.LocalAddress(%v)\n", i, conn.LocalAddr())
    fmt.Fprintf(conn, "[%d]: hi\n", i)  // writes "[0]: hi\n" or "[1]: hi\n"
    
    sc := bufio.NewScanner(conn)
    sc.Scan()
    fmt.Println(sc.Text())
    pool.ReturnConn(conn)
}
</code></pre></div><p>The <code>[0]</code>, <code>[1]</code>, <code>[2]</code> in the logs are the iteration numbers from the loops. They&#8217;re not goroutine IDs (the server is single-threaded in the client, looping sequentially). The server echoes back whatever it receives, so <code>[0]: hi</code> in the request becomes <code>[0]: hi</code> in the response.</p><p>Server-side code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">listener, _ := net.Listen("tcp", ":4040")
i := 0
for {
    conn, _ := listener.Accept()
    go func(conn net.Conn, gri int) {
        defer conn.Close()
        sc := bufio.NewScanner(conn)
        who := conn.RemoteAddr()
        
        log.Printf("[go-routine-%d][%s]: connected\n", gri, who)
        
        for sc.Scan() {
            t := sc.Text()
            log.Printf("[go-routine-%d][%s]: %s\n", gri, who, t)
            fmt.Fprintf(conn, "[go-routine-%d]echo: %s\n", gri, t)  // echo back
        }
        
        log.Printf("[go-routine-%d][%s]: disconnected\n", gri, who)
    }(conn, i)
    i++
}
</code></pre></div><p>The <code>gri</code> (goroutine ID) is just an incrementing counter captured at <code>Accept()</code> time. Each new connection gets the next number. The server stays in the <code>for sc.Scan()</code> loop, handling multiple requests on the same connection until the client disconnects or the connection is closed.</p><p>Client logs showed this:</p><pre><code><code>iter[0]: conn.LocalAddress(127.0.0.1:55645)
[go-routine-2]echo: [0]: hi
iter[1]: conn.LocalAddress(127.0.0.1:55646)
[go-routine-3]echo: [1]: hi
iter[2]: conn.LocalAddress(127.0.0.1:55647)
[go-routine-4]echo: [2]: hi
iter[0]: conn.LocalAddress(127.0.0.1:55645)   &#8592; same port, reused
[go-routine-2]echo: [0]: hi
iter[1]: conn.LocalAddress(127.0.0.1:55646)   &#8592; same port, reused
[go-routine-3]echo: [1]: hi
iter[2]: conn.LocalAddress(127.0.0.1:55647)   &#8592; same port, reused
[go-routine-4]echo: [2]: hi
iter[3]: conn.LocalAddress(127.0.0.1:55643)   &#8592; overflow connection
[go-routine-0]echo: [3]: hi
iter[4]: conn.LocalAddress(127.0.0.1:55644)   &#8592; overflow connection
[go-routine-1]echo: [4]: hi
</code></code></pre><p>Server logs showed this:</p><pre><code><code>[go-routine-0][127.0.0.1:55643]: connected
[go-routine-1][127.0.0.1:55644]: connected
[go-routine-2][127.0.0.1:55645]: connected
[go-routine-3][127.0.0.1:55646]: connected
[go-routine-4][127.0.0.1:55647]: connected
[go-routine-2][127.0.0.1:55645]: [0]: hi
[go-routine-3][127.0.0.1:55646]: [1]: hi
[go-routine-4][127.0.0.1:55647]: [2]: hi
[go-routine-2][127.0.0.1:55645]: [0]: hi   &#8592; same goroutine, second request
[go-routine-3][127.0.0.1:55646]: [1]: hi   &#8592; same goroutine, second request
[go-routine-4][127.0.0.1:55647]: [2]: hi   &#8592; same goroutine, second request
[go-routine-0][127.0.0.1:55643]: [3]: hi
[go-routine-0][127.0.0.1:55643]: disconnected                  &#8592; immediately, pool full
[go-routine-1][127.0.0.1:55644]: [4]: hi
[go-routine-1][127.0.0.1:55644]: disconnected                  &#8592; immediately, pool full
[go-routine-4][127.0.0.1:55647]: disconnected                  &#8592; on process exit
[go-routine-2][127.0.0.1:55645]: disconnected                  &#8592; on process exit
[go-routine-3][127.0.0.1:55646]: disconnected                  &#8592; on process exit
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nbOW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nbOW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 424w, https://substackcdn.com/image/fetch/$s_!nbOW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 848w, https://substackcdn.com/image/fetch/$s_!nbOW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 1272w, https://substackcdn.com/image/fetch/$s_!nbOW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nbOW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png" width="1456" height="924" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:924,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:212023,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/197137595?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nbOW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 424w, https://substackcdn.com/image/fetch/$s_!nbOW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 848w, https://substackcdn.com/image/fetch/$s_!nbOW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 1272w, https://substackcdn.com/image/fetch/$s_!nbOW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f642698-1c3e-4cbe-a8b3-66aee685c3c2_2342x1486.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Reading the logs step by step:</p><p><strong>Initial dial (5 connections created):</strong></p><ul><li><p>Ports 55643, 55644 &#8212; the two overflow connections (<code>conn3</code>, <code>conn4</code>)</p></li><li><p>Ports 55645, 55646, 55647 &#8212; the three warm-up connections, returned to pool</p></li></ul><p><strong>First loop (iter 0, 1, 2):</strong></p><ul><li><p>Client grabbed ports 55645, 55646, 55647 from pool</p></li><li><p>Same ports = same <code>net.Conn</code> = zero new dials</p></li><li><p>Server: go-routine-2, 3, 4 handle these requests</p></li><li><p>Client returns all 3 to pool after use</p></li></ul><p><strong>Second loop (iter 0, 1, 2 again):</strong></p><ul><li><p>Client grabbed the same three ports again from pool</p></li><li><p>Server: go-routine-2, 3, 4 handle their <strong>second request</strong> on the same connection</p></li><li><p>This is connection reuse in action: same goroutine, same connection, two separate request-response cycles</p></li><li><p>Client returns all 3 to pool again</p></li></ul><p><strong>Overflow (iter 3, 4):</strong></p><ul><li><p>Client calls <code>handleConn(3, conn3, pool)</code> and <code>handleConn(4, conn4, pool)</code></p></li><li><p>Both write their request, get a response, call <code>ReturnConn</code></p></li><li><p>Pool is already full (3/3)</p></li><li><p><code>ReturnConn</code> hits the <code>default</code> case: <code>c.Close()</code></p></li><li><p>Server receives FIN on ports 55643 and 55644</p></li><li><p>go-routine-0 and go-routine-1 wake from <code>sc.Scan()</code>, see EOF, loop exits</p></li><li><p>&#8220;disconnected&#8221; logs fire <strong>immediately</strong> after handling the request</p></li></ul><p><strong>Process exit:</strong></p><ul><li><p>Client process exits</p></li><li><p>OS closes all remaining fds (55645, 55646, 55647)</p></li><li><p>FINs go out</p></li><li><p>go-routine-2, 3, 4 wake from <code>sc.Scan()</code>, see EOF, disconnect</p></li></ul><p>The key proof: go-routine-2 handled <strong>two</strong> requests on port 55645 (iter[0] in both loops). Same ephemeral port = same TCP connection = same kernel <code>tcp_sock</code> structure = zero new handshakes. The connection was returned to the pool after the first loop and grabbed again in the second loop.</p><p>go-routine-0 and go-routine-1 disconnected immediately because <code>ReturnConn</code> closed them, not because the server decided to. The pool was full, the <code>default</code> case fired, <code>c.Close()</code> sent a FIN.</p><h2>Adding MaxConnsPerHost</h2><p>A buffered channel pool has no burst protection. 1000 goroutines hitting <code>GetConn</code> simultaneously dial 1000 connections to the server at once.</p><p>Fix: a semaphore.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">// pre-filled with MaxConnsPerHost tokens
maxConnsPerHost := make(chan struct{}, MaxConnsPerHost)
for i := 0; i &lt; MaxConnsPerHost; i++ {
    maxConnsPerHost &lt;- struct{}{}
}

// GetConn: acquire token before dialing
&lt;-p.MaxConnsPerHost  // blocks when at limit, parks goroutine

// ReturnConn: release token when connection is closed
p.MaxConnsPerHost &lt;- struct{}{}
</code></pre></div><p><code>chan struct{}</code> not <code>chan int</code>. <code>struct{}</code> is zero-size, no allocation per token.</p><p>I ran 10 goroutines with <code>MaxConnsPerHost=6</code>:</p><pre><code><code>02:26:36 [go-routine-6] blocking for MaxConnsPerHost   &#8592; 4 parked immediately
02:26:36 [go-routine-7] blocking for MaxConnsPerHost
02:26:36 [go-routine-8] blocking for MaxConnsPerHost
02:26:36 [go-routine-4] blocking for MaxConnsPerHost
02:26:36 [go-routine-1] using conn.LocalAddress(127.0.0.1:58963)   &#8592; 6 dial
02:26:36 [go-routine-3] using conn.LocalAddress(127.0.0.1:58964)
02:26:36 [go-routine-2] using conn.LocalAddress(127.0.0.1:58966)
02:26:36 [go-routine-5] using conn.LocalAddress(127.0.0.1:58967)
02:26:36 [go-routine-0] using conn.LocalAddress(127.0.0.1:58965)
02:26:36 [go-routine-9] using conn.LocalAddress(127.0.0.1:58968)
                                                         &#8592; 2 seconds later
02:26:38 [go-routine-6] using conn.LocalAddress(127.0.0.1:58971)   &#8592; 3 unblock
02:26:38 [go-routine-8] using conn.LocalAddress(127.0.0.1:58970)
02:26:38 [go-routine-7] using conn.LocalAddress(127.0.0.1:58969)
</code></code></pre><p>Wave pattern: burst hits cap, excess goroutines park on the semaphore. As in-flight connections complete and return, parked goroutines unblock in waves.</p><h2>IdleConnTimeout</h2><p>Idle connections sitting forever create a problem: servers close them after their own timeout, sending a FIN. Client grabs the stale connection, writes a request, gets an error.</p><p>Fix: timestamp each connection on return. A background goroutine wakes periodically and evicts connections that have been idle too long.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type ConnPool struct {
    mu   sync.Mutex
    pool []struct {
        idleSince time.Time
        conn      net.Conn
    }
    MaxIdleConnsPerHost int
    MaxConnsPerHost     chan struct{}
    IdleConnTimeout     time.Duration
}
</code></pre></div><p>Why switch from channel to slice: a buffered channel can&#8217;t be peeked without consuming. To check expiry, you need to inspect elements without removing them. A slice as a FIFO queue works: append at the back (newest last), pop from back for <code>GetConn</code> (warmest first), check the front for eviction (oldest first).</p><p>Eviction interval: <code>IdleConnTimeout / 2</code>. Worst case: connection becomes idle just after a wakeup. One interval later it&#8217;s checked, age is <code>IdleConnTimeout / 2</code>. One more interval, evicted. Maximum overshoot: one interval. Go&#8217;s <code>net/http</code> uses exactly this internally.</p><p>I set <code>IdleConnTimeout = 5</code> seconds for testing. After the client exited, the eviction goroutine woke 5 seconds later:</p><pre><code><code>2026/05/10 00:32:04 [pool-idle-conn-eviction] closing conn: 127.0.0.1:57050
2026/05/10 00:32:04 [pool-idle-conn-eviction] closing conn: 127.0.0.1:57051
2026/05/10 00:32:04 [pool-idle-conn-eviction] closing conn: 127.0.0.1:57052
</code></code></pre><p>Server logs confirmed all 3 goroutines woke from <code>sc.Scan()</code> and disconnected at the same timestamp. The eviction goroutine freed them, not process shutdown.</p><p>Always set <code>IdleConnTimeout</code> below the server&#8217;s timeout. If the server times out first and sends a FIN, the client&#8217;s next attempt on that stale connection will error.</p><h2>Dedicated-Host vs Shared-Host</h2><p>The pool I built so far has <code>Addr string</code> hardcoded at construction. One pool per target server. This is a dedicated-host pool.</p><p>All knobs (<code>MaxIdleConnsPerHost</code>, <code>MaxConnsPerHost</code>, <code>IdleConnTimeout</code>) scope naturally to that one host. Simple, predictable.</p><p>A shared-host pool, one pool instance for all hosts, needs different structure. The flat slice becomes a map:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">pool map[string][]struct {
    idleSince time.Time
    conn      net.Conn
}
</code></pre></div><p><code>MaxConnsPerHost</code> becomes <code>map[string] &#8594; chan struct{}</code>, one independent semaphore per host.</p><p>Now two scopes appear:</p><ul><li><p><code>MaxIdleConnsPerHost</code> (per-host), caps stale idle accumulation on any one host</p></li><li><p><code>MaxIdleConns</code> (global), caps total idle connections across all hosts</p></li></ul><p>Without the global cap, a client talking to 50 hosts with <code>MaxIdleConnsPerHost=10</code> could hold 500 idle fds, most unused.</p><h2>The Semaphore Invariant</h2><p>Three operations touch both semaphores (<code>IdleConns</code> global, <code>MaxConns[addr]</code> per-host). The core invariant:</p><pre><code><code>tokens_in_IdleConns + connections_in_pool = MaxIdleConns  (always)
</code></code></pre><p>Operation <code>MaxConns[addr]</code> <code>IdleConns</code> global Dial new conn consume 1 no change ReturnConn &#8594; pool no change consume 1 (entering idle) ReturnConn &#8594; per-host cap release 1 (closed) no change ReturnConn &#8594; global cap release 1 (closed) no change GetConn &#8594; pool hit no change release 1 (leaving idle) Eviction release 1 (closed) release 1 (leaving idle)</p><p>Eviction must release both. An evicted connection held a per-host slot (consumed at dial) and a global idle slot (consumed at <code>ReturnConn</code>). Releasing only one causes permanent token starvation.</p><h2>The Knobs</h2><p><code>http.Transport</code> exposes the same knobs I built:</p><ul><li><p><code>MaxIdleConnsPerHost</code>, warm idle connections per host. Default: 2 (very conservative). First thing to tune under high load.</p></li><li><p><code>MaxIdleConns</code>, global ceiling across all hosts. Only relevant when one <code>http.Client</code> talks to multiple hosts.</p></li><li><p><code>MaxConnsPerHost</code>, hard ceiling on total connections (idle + in-use). When hit, new requests block waiting. Default: 0 (no limit).</p></li><li><p><code>IdleConnTimeout</code>, evict idle connections after this duration. Default: 0 (connections sit forever, vulnerable to server-side RST).</p></li></ul><p><code>MaxConnsPerHost</code> protects the server from being overwhelmed. Set it higher than <code>MaxIdleConnsPerHost</code> to absorb burst: extra connections created under load get closed on <code>ReturnConn</code> (pool already full), naturally draining back to the idle cap.</p><p>There&#8217;s no global <code>MaxConns</code>. A global connection limit across all hosts has no coherent meaning. Throttling connections to <code>api.stripe.com</code> because you have too many open to <code>api.github.com</code> protects neither server.</p><h2>SQLAlchemy Mapping</h2><p>I recently worked on Airflow where many parallel DAG tasks were connecting to Postgres simultaneously. The default config was throwing connection errors.</p><p>The fix was tuning two SQLAlchemy knobs:</p><pre><code><code>AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_SIZE: "20"
AIRFLOW__DATABASE__SQL_ALCHEMY_MAX_OVERFLOW: "20"
</code></code></pre><p>These map directly:</p><ul><li><p><code>POOL_SIZE</code> = <code>MaxIdleConnsPerHost</code>, idle connections kept ready</p></li><li><p><code>MAX_OVERFLOW</code> = extra connections allowed beyond pool size</p></li><li><p><code>MaxConnsPerHost</code> = <code>POOL_SIZE + MAX_OVERFLOW</code> = 40 total</p></li></ul><p>Airflow defaults: <code>POOL_SIZE = 5</code>, <code>MAX_OVERFLOW = 5</code>, 10 total. Under parallel DAG execution that exhausts fast.</p><p>Same semaphore pattern, same wave behavior, different language.</p><h2>HTTP Framing</h2><p>HTTP is the same pool, with a self-describing framing protocol.</p><p>Raw TCP framing:</p><ul><li><p>Request end: <code>\n</code></p></li><li><p>Response end: implicit, one line echoed back</p></li><li><p>Connection fate: server loop stays open</p></li></ul><p>HTTP framing:</p><ul><li><p>Request end: <code>\r\n\r\n</code> after headers + <code>Content-Length</code> bytes</p></li><li><p>Response end: <code>Content-Length</code> bytes read OR <code>0\r\n\r\n</code> in chunked encoding</p></li><li><p>Connection fate: <code>Connection</code> header decides</p></li></ul><p><code>http.Transport</code> is a shared-host pool. The same knobs map directly: <code>MaxIdleConnsPerHost</code>, <code>MaxIdleConns</code>, <code>MaxConnsPerHost</code>, <code>IdleConnTimeout</code>.</p><p>HTTP/1.1 default is keep-alive. <code>Connection: close</code> is the opt-out. <code>Connection: keep-alive</code> is redundant. I verified this by removing it from the server, nothing observable changed in pool behavior.</p><p><code>resp.Body.Close()</code> is not optional. Without draining and closing the body, <code>http.Transport</code> doesn&#8217;t know the response is complete. The connection can&#8217;t be returned to the pool. It leaks.</p><h2>HOL Blocking</h2><p>HTTP/1.1 with keep-alive: one connection, one request in-flight at a time.</p><p>I built a server that sleeps 5 seconds when the request body is <code>"slow"</code>. Sent 10 sequential requests: <code>fast, fast, fast, fast, slow, fast, fast, fast, fast, fast</code>.</p><pre><code><code>22:54:46  ConnectStart &#8594; :53594          &#8592; one dial, one connection
22:54:46  GotConn :53594 &#8594; fast          &#8592; instant
22:54:46  GotConn :53594 &#8594; fast
22:54:46  GotConn :53594 &#8594; fast
22:54:46  GotConn :53594 &#8594; fast
22:54:46  GotConn :53594 &#8594; [waiting]     &#8592; slow request holds the connection
22:54:51  response: slow                 &#8592; 5 seconds later
22:54:51  GotConn :53594 &#8594; fast          &#8592; all unblock instantly
</code></code></pre><p>One slow request blocked five fast requests. Not because the server was busy but because they all shared one connection with no way to label which response belongs to which request.</p><p>The root cause: ordering without identity. The TCP byte stream has no per-request label. Responses must come back in the same order as requests. Fix: HTTP/2 adds a 31-bit stream ID to every frame. The receiver reassembles each response independently. One connection, N concurrent streams, no HOL at the application layer.</p><p>The pool design determines HOL exposure: one idle connection = full HOL, more connections = isolated per-connection, HTTP/2 = eliminated at app layer (TCP HOL remains).</p><h2>What&#8217;s Still Open</h2><p>I now understand what a connection pool is and how it decides when a connection is safe to reuse. The framing contract is inseparable from pooling. The knobs (<code>MaxIdleConnsPerHost</code>, <code>MaxConnsPerHost</code>, <code>IdleConnTimeout</code>) map directly between raw TCP pools and <code>http.Transport</code>.</p><p>What I still don&#8217;t understand well enough: how HTTP/2 multiplexing actually works at the frame level. I know it uses stream IDs, but I want to see the bytes on the wire. How QUIC eliminates TCP HOL blocking by implementing per-stream reliability over UDP. What the trade-offs are between dedicated-host pools (one per service) and shared-host pools in a microservice environment.</p><p>That&#8217;s where this goes next.</p>]]></content:encoded></item><item><title><![CDATA[Go's Netpoller Handles Millions of Connections. But Connections Still Aren't Free.]]></title><description><![CDATA[A deep dive into TCP connection lifecycle, how Go's netpoller parks goroutines instead of blocking threads, and why connection pools exist.]]></description><link>https://logs.craftedbyvishal.dev/p/gos-netpoller-handles-millions-of</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/gos-netpoller-handles-millions-of</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Sun, 03 May 2026 11:03:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!0gZl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A while back I wrote my first Substack article about building a raw TCP server in Go. You can read it <a href="https://vishalageek.substack.com/p/i-thought-go-concurrency-was-about">here</a>. It ended here:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func main() {
    lis, err := net.Listen("tcp", ":8080")
    // ...

    for {
        conn, err := lis.Accept()
        // ...

        go func(conn net.Conn) {
            sc := bufio.NewScanner(conn)
            who := conn.RemoteAddr()

            for sc.Scan() {
                fmt.Printf("[%s]: %s\n", who, sc.Text())
            }
            // ...
        }(conn)
    }
}</code></pre></div><p>Concurrent. Each client got its own goroutine. The server stopped being unavailable.</p><p>But I had a question I couldn&#8217;t shake.</p><p>What actually happens inside that goroutine? What is <code>conn</code>? What did <code>net.Listen</code> do before <code>Accept()</code> was even called? What does the kernel know about this connection that my code doesn&#8217;t?</p><p>I knew the textbook answer. In college I drew this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TysG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TysG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 424w, https://substackcdn.com/image/fetch/$s_!TysG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 848w, https://substackcdn.com/image/fetch/$s_!TysG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 1272w, https://substackcdn.com/image/fetch/$s_!TysG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TysG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png" width="1456" height="1472" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1472,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3185362,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TysG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 424w, https://substackcdn.com/image/fetch/$s_!TysG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 848w, https://substackcdn.com/image/fetch/$s_!TysG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 1272w, https://substackcdn.com/image/fetch/$s_!TysG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98c06e5e-899c-48f2-b008-5949c37597ff_8840x8939.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>SYN, SYN-ACK, ACK. Three-way handshake. Four-way teardown. The state machine labels on the left for the client, on the right for the server. I could reproduce those arrows from memory.</p><p>What I never understood was what each arrow cost. What got allocated when the SYN arrived. Who actually sent the SYN-ACK. What Go does when you call <code>conn.Read()</code> and there&#8217;s nothing there yet. What TIME_WAIT actually is and why it exists.</p><p>I wanted to see this at the kernel level, not in theory, but through code I wrote and understood line by line.</p><p>So I built a raw TCP echo server and client in Go, no <code>net/http</code>, no frameworks, just <code>net.Dial</code>, <code>net.Listen</code>, <code>net.Conn</code>, and drew out every layer until I understood it.</p><p>This is what I found.</p><div><hr></div><h2>First, What Is a File Descriptor</h2><p>Before any of this makes sense, one concept is worth grounding first.</p><p>A file descriptor is just an integer. <code>fd=5</code>, <code>fd=10</code>, <code>fd=3</code>. That&#8217;s all it is on the surface.</p><p>Every process has its own per-process file descriptor table, a kernel-maintained mapping from these integers to actual resources in kernel memory. Those resources can be files on disk, pipes, or in our case, sockets. <code>fd=5</code> in our Go server process points to one socket. <code>fd=5</code> in a completely different process points to something else entirely. The integer means nothing across process boundaries. Within a process, it is unique.</p><p>When you call <code>socket()</code>, the kernel allocates a <code>tcp_sock</code> structure in RAM and returns the next available integer from our process&#8217;s fd table. That number is your handle. Every syscall you make, <code>bind()</code>, <code>listen()</code>, <code>accept()</code>, <code>read()</code>, <code>write()</code>, <code>close()</code>, takes that integer and the kernel looks up the actual resource behind it.</p><p>This is why &#8220;frees fd=10 from the process fd table&#8221; during teardown is significant. The integer <code>10</code> is released back to the table. The kernel can hand it to the next <code>accept()</code> call for a completely different client. The underlying socket structure may still exist in kernel memory, in TIME_WAIT, but our process has no handle to it anymore. The fd and the socket are two different things that happen to be linked for the duration of a connection.</p><div><hr></div><h2>A Connection Is Not What I Thought It Was</h2><p>Before I wrote any code, I assumed a &#8220;connection&#8221; was the thing <code>listener.Accept()</code> returned.</p><p>It&#8217;s not. Or rather, that&#8217;s the end of a much longer process.</p><p>When you call <code>net.Listen("tcp", ":4040")</code>, three syscalls happen in sequence.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NDFo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NDFo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 424w, https://substackcdn.com/image/fetch/$s_!NDFo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 848w, https://substackcdn.com/image/fetch/$s_!NDFo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 1272w, https://substackcdn.com/image/fetch/$s_!NDFo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NDFo!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png" width="1200" height="2544.8630136986303" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:2477,&quot;width&quot;:1168,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:310040,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NDFo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 424w, https://substackcdn.com/image/fetch/$s_!NDFo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 848w, https://substackcdn.com/image/fetch/$s_!NDFo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 1272w, https://substackcdn.com/image/fetch/$s_!NDFo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa06f129-ea16-4e88-8b5f-891b02afed69_1168x2477.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>socket()</code> asks the kernel to create a TCP socket, a data structure in kernel memory, and hand back a file descriptor. That fd, say <code>fd=5</code>, is just a number. A handle. The socket itself lives in the kernel. At this point it has no address, no port. It just exists.</p><p><code>bind(fd, "0.0.0.0", 4040)</code> attaches the socket to a port. The kernel checks if port 4040 is already in use, checks process permissions, then updates its internal port-to-socket mapping. Now <code>fd=5</code> owns <code>:4040</code>.</p><p><code>listen(fd)</code> is where something interesting happens. The socket transitions from a plain socket to a listening socket, and the kernel creates two queues:</p><pre><code><code>tcp_sock
state: LISTEN
local: :4040
syn_queue:    []
accept_queue: []</code></code></pre><p>These two queues are the kernel&#8217;s waiting room for incoming clients. <code>net.Listen</code> returns. Our Go code has a listener. Nothing has connected yet.</p><p>And then our goroutine calls <code>listener.Accept()</code>. If the accept queue is empty, it parks. How exactly &#8212; that's what the next section is about. But the short version: Go's netpoller watches the fd on our behalf, and wakes the goroutine when something arrives. More on the netpoller in detail further below.</p><div><hr></div><h2>The Handshake Happens Without You</h2><p>When a client sends a SYN packet, our goroutine is not involved.</p><p>Not partially involved. Not notified. Completely absent.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PFNi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PFNi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 424w, https://substackcdn.com/image/fetch/$s_!PFNi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 848w, https://substackcdn.com/image/fetch/$s_!PFNi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 1272w, https://substackcdn.com/image/fetch/$s_!PFNi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PFNi!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png" width="1200" height="1716.7582417582419" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e7575a44-67aa-430e-9565-affb48853985_1820x2604.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:2083,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:453113,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PFNi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 424w, https://substackcdn.com/image/fetch/$s_!PFNi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 848w, https://substackcdn.com/image/fetch/$s_!PFNi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 1272w, https://substackcdn.com/image/fetch/$s_!PFNi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7575a44-67aa-430e-9565-affb48853985_1820x2604.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The SYN packet arrives at the NIC. The NIC fires a hardware interrupt. The kernel&#8217;s interrupt handler picks it up. The packet walks up the network stack, Ethernet &#8594; IP &#8594; TCP. The kernel looks up which socket owns the destination port. It finds <code>fd=5</code>.</p><p>Then the kernel creates a new socket for this specific client, <code>cli:tcp_sock</code>, with the client&#8217;s address already filled in, and places it in the SYN queue. Sends SYN-ACK. When the client&#8217;s ACK arrives, the kernel moves <code>cli:tcp_sock</code> to the accept queue and marks it ESTABLISHED.</p><p>Our goroutine is parked the entire time. The three-way handshake completes before <code>Accept()</code> returns. The goroutine only wakes up to collect the result.</p><p>That was the first thing that surprised me. I had always imagined my server code as participating in the handshake. It doesn&#8217;t. The kernel does all of it.</p><p>Look at the diagram above again. The <code>g_server</code> column is completely empty throughout the handshake. That emptiness is the point.</p><p>There&#8217;s one more detail worth noting. The kernel creates <code>cli:tcp_sock</code> when the SYN arrives, not when the ACK arrives. It pre-allocates the socket structure mid-handshake. The connection only becomes real, both sides agreeing on sequence numbers, when the final ACK lands. But the kernel commits resources earlier.</p><div><hr></div><h2>What &#8220;Blocking&#8221; Actually Means in Go</h2><p>This is where Go&#8217;s netpoller comes in.</p><p>The netpoller is a component of the Go runtime that sits between our goroutines and the Linux kernel&#8217;s epoll mechanism. Its job is simple: watch file descriptors for I/O readiness events, and when one becomes ready, wake up whichever goroutine was waiting on it. It runs on its own OS thread, calling <code>epoll_wait()</code> in a loop. Our goroutines never call <code>epoll_wait()</code> directly. They park, and the netpoller does the watching.</p><p>This is what makes Go capable of handling millions of concurrent connections without millions of OS threads. The netpoller converts what looks like blocking network I/O in your code into event-driven I/O underneath &#8212; our goroutine writes <code>conn.Read()</code> as if it blocks, but the runtime never actually blocks the OS thread. Instead it parks the goroutine and frees the thread to run other goroutines. The OS thread stays available for CPU work or other connections while the kernel watches the fd. When data arrives, the netpoller wakes the right goroutine and the thread picks it up again.</p><p>Park goroutines. Free threads. Handle more. That&#8217;s the model.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V3Xr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V3Xr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 424w, https://substackcdn.com/image/fetch/$s_!V3Xr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 848w, https://substackcdn.com/image/fetch/$s_!V3Xr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 1272w, https://substackcdn.com/image/fetch/$s_!V3Xr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V3Xr!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png" width="1200" height="1582.4175824175825" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd378160-5d67-467e-a833-abc383ff2173_1549x2043.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1920,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:261845,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!V3Xr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 424w, https://substackcdn.com/image/fetch/$s_!V3Xr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 848w, https://substackcdn.com/image/fetch/$s_!V3Xr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 1272w, https://substackcdn.com/image/fetch/$s_!V3Xr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd378160-5d67-467e-a833-abc383ff2173_1549x2043.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The netpoller tracks which goroutine is waiting on which fd using a <code>pollDesc</code> &#8212; a small struct it maintains for every registered fd:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type pollDesc struct {
    fd int
    rg *g  // goroutine waiting to read
    wg *g  // goroutine waiting to write
}</code></pre></div><p>This is the bridge between a file descriptor and a goroutine. When <code>g_server</code> calls <code>Accept()</code> and the accept queue is empty, the full sequence is:</p><ul><li><p>The runtime stores <code>g_server</code> in <code>pollDesc[fd=5].rg</code></p></li><li><p>Registers <code>fd=5</code> with epoll via <code>epoll_ctl(ADD, fd=5, EPOLLIN | EPOLLET)</code></p></li><li><p>Calls <code>gopark()</code> &#8212; <code>g_server</code> is removed from the run queue, the scheduler&#8217;s list of goroutines ready to execute</p></li><li><p>The OS thread that was running <code>g_server</code> is freed and picks up other goroutines</p></li></ul><p>When the handshake completes and something lands in the accept queue, the netpoller&#8217;s <code>epoll_wait()</code> returns <code>fd=5</code> as ready. The netpoller looks up <code>pollDesc[fd=5].rg</code>, finds <code>g_server</code>, clears the field, and calls <code>runqueue.add(g_server)</code> &#8212; putting it back on the scheduler&#8217;s run queue. An OS thread picks it up and resumes it exactly where it parked, inside <code>Accept()</code>, as if nothing happened in between.</p><p><code>Accept()</code> makes the syscall. The kernel dequeues <code>cli:tcp_sock</code> from the accept queue and returns <code>conn (fd=10)</code>.</p><p><code>g_server</code> immediately calls <code>go handleConn(conn)</code>, spawning <code>g_conn_1</code> with ownership of the connection, and loops back to <code>listener.Accept()</code>. The infinite for loop. The server is available again before <code>g_conn_1</code> has done anything at all.</p><p>This is the whole model:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func main() {
    l, err := net.Listen("tcp", ":4040")
    // ...
    for {
        conn, err := l.Accept()
        // ...
        go handleConn(conn)
    }
}</code></pre></div><p><code>g_server</code> parks at <code>Accept()</code>. Kernel does the handshake. Netpoller wakes <code>g_server</code>. <code>g_server</code> spawns <code>g_conn_1</code> and parks again. No OS thread sleeps waiting for a client. No thread is wasted.</p><div><hr></div><h2>Inside the Connection Handler</h2><p><code>g_conn_1</code> takes over <code>conn (fd=10)</code> and calls <code>conn.Read(buf)</code>. There&#8217;s no data yet, the client hasn&#8217;t sent anything. Same pattern repeats.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xCxG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xCxG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 424w, https://substackcdn.com/image/fetch/$s_!xCxG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 848w, https://substackcdn.com/image/fetch/$s_!xCxG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 1272w, https://substackcdn.com/image/fetch/$s_!xCxG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xCxG!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png" width="1200" height="1224.07221664995" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1017,&quot;width&quot;:997,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:112693,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xCxG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 424w, https://substackcdn.com/image/fetch/$s_!xCxG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 848w, https://substackcdn.com/image/fetch/$s_!xCxG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 1272w, https://substackcdn.com/image/fetch/$s_!xCxG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b61088-29be-40b9-8b88-720bee9dfb83_997x1017.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The Go runtime registers <code>fd=10</code> with epoll, <code>epoll_ctl(ADD, fd=10, EPOLLIN | EPOLLET)</code>, stores <code>g_conn_1</code> in <code>pollDesc[fd=10].rg</code>, and parks it.</p><p>Now look at the pollDesc table:</p><pre><code><code>{fd:5,  rg: g_server,  wg: nil}
{fd:10, rg: g_conn_1,  wg: nil}</code></code></pre><p>Two goroutines. Two fds. One epoll instance. Zero OS threads consumed by either.</p><p>This is why Go can handle tens of thousands of concurrent connections with a handful of OS threads. Every goroutine waiting for I/O is just a data structure in memory. The OS threads are free.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LWBG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LWBG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 424w, https://substackcdn.com/image/fetch/$s_!LWBG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 848w, https://substackcdn.com/image/fetch/$s_!LWBG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 1272w, https://substackcdn.com/image/fetch/$s_!LWBG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LWBG!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png" width="1200" height="1771.978021978022" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:2150,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:385579,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LWBG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 424w, https://substackcdn.com/image/fetch/$s_!LWBG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 848w, https://substackcdn.com/image/fetch/$s_!LWBG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 1272w, https://substackcdn.com/image/fetch/$s_!LWBG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff244cb7-ae1d-41ee-8baa-4ad7d353a204_1765x2606.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When the client sends data, the NIC fires an interrupt. The kernel places the bytes in <code>rcv_buff</code> of <code>fd=10</code>, doing TCP reassembly along the way, ordering out-of-order segments, discarding duplicates. The kernel marks <code>fd=10</code> as ready in epoll. The netpoller&#8217;s <code>epoll_wait</code> returns <code>fd=10</code>. The netpoller finds <code>g_conn_1</code> in <code>pollDesc[fd=10].rg</code> and puts it back on the run queue. <code>g_conn_1</code> resumes, <code>conn.Read(buf)</code> drains from <code>rcv_buff</code> into <code>buf</code>, and the handler processes the request.</p><div><hr></div><h2>The Client Side &#8212; EINPROGRESS</h2><p>Building the client taught me the mirror image of all of this.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZAuS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZAuS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 424w, https://substackcdn.com/image/fetch/$s_!ZAuS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 848w, https://substackcdn.com/image/fetch/$s_!ZAuS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 1272w, https://substackcdn.com/image/fetch/$s_!ZAuS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZAuS!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png" width="1200" height="1440.3314917127072" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1738,&quot;width&quot;:1448,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:272047,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZAuS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 424w, https://substackcdn.com/image/fetch/$s_!ZAuS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 848w, https://substackcdn.com/image/fetch/$s_!ZAuS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 1272w, https://substackcdn.com/image/fetch/$s_!ZAuS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fd0ec6d-b05a-451f-a2fa-988486a4661c_1448x1738.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>net.Dial("tcp", ":4040")</code> calls <code>socket()</code> then <code>connect()</code>. No <code>bind()</code> call, the kernel assigns an ephemeral port automatically from the range 32768&#8211;60999. So the client&#8217;s socket gets <code>local: :54321, remote: :4040</code> without our code ever choosing the port number.</p><p>But here&#8217;s where it diverges from the server side. The netpoller registers <code>fd=3</code> with epoll for <code>EPOLLOUT</code>, not <code>EPOLLIN</code>. And the pollDesc entry is:</p><pre><code><code>{fd:3, wg: g_client, rg: nil}</code></code></pre><p><code>wg</code> not <code>rg</code>. Write goroutine, not read goroutine.</p><p><code>connect()</code> on a non-blocking socket returns immediately with <code>EINPROGRESS</code>, handshake started, not done yet. The goroutine is parked.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VTEa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VTEa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 424w, https://substackcdn.com/image/fetch/$s_!VTEa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 848w, https://substackcdn.com/image/fetch/$s_!VTEa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 1272w, https://substackcdn.com/image/fetch/$s_!VTEa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VTEa!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png" width="1200" height="2067.032967032967" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:2508,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:401553,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VTEa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 424w, https://substackcdn.com/image/fetch/$s_!VTEa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 848w, https://substackcdn.com/image/fetch/$s_!VTEa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 1272w, https://substackcdn.com/image/fetch/$s_!VTEa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5db89b7-4ac5-4550-8b7d-aabd569a1067_1732x2984.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When the SYN-ACK arrives, the kernel completes the handshake and marks <code>fd=3</code> as writable. For a connecting socket, writable means connected. The send buffer is ready, which only becomes true once the connection is established. The kernel reuses &#8220;writable&#8221; as the signal for &#8220;you are now connected.&#8221;</p><p>The netpoller sees <code>fd=3</code> is ready via EPOLLOUT, finds <code>g_client</code> in <code>pollDesc[fd=3].wg</code>, puts it back on the run queue. <code>g_client</code> resumes at <code>connect()</code>, Go checks <code>getsockopt(SO_ERROR)</code> to confirm the connection actually succeeded, and <code>net.Dial</code> returns <code>conn (fd=3)</code>.</p><p>The client is now ESTABLISHED. The first <code>conn.Write([]byte)</code> puts bytes into <code>send_buff</code>. The kernel drains <code>send_buff</code> onto the wire.</p><div><hr></div><h2>Building a Raw TCP Echo Server and Client</h2><p>To understand teardown properly, I needed both sides of the connection. So I built a raw TCP echo server and client, no <code>net/http</code>, no frameworks, just <code>net.Listen</code>, <code>net.Dial</code>, and <code>net.Conn</code> directly.</p><p>The server spawns a goroutine per connection, reads lines via <code>bufio.Scanner</code>, and echoes each one back:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">go func(conn net.Conn) {
    defer conn.Close()
    sc := bufio.NewScanner(conn)
    who := conn.RemoteAddr()

    for sc.Scan() {
        t := sc.Text()
        fmt.Fprintf(conn, "echo: %s\n", t)
    }
    // ...
}(conn)</code></pre></div><p><code>defer conn.Close()</code> is important. Without it, the goroutine exits but the kernel-side socket stays open. The client&#8217;s reader blocks forever waiting for data that never comes. The fd leaks. Over time fds accumulate, the process hits its fd limit, and <code>Accept()</code> starts failing. <code>defer</code> covers all exit paths cleanly.</p><p>The client reads from stdin and writes to the connection, and simultaneously reads echoes back from the server and prints them to stdout.</p><p>That &#8220;simultaneously&#8221; is the problem. Two directions, one connection.</p><p>Run them sequentially and the first blocks forever.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">done := make(chan any, 1)

// writer: stdin &#8594; conn
go func() {
    io.Copy(conn, WrappedReader{r: os.Stdin})
    conn.(*net.TCPConn).CloseWrite()
}()

// reader: conn &#8594; stdout
go func() {
    io.Copy(os.Stdout, conn)
    done &lt;- 1
}()

&lt;-done
conn.Close()</code></pre></div><p>The reader signals <code>done</code> when it finishes. Main waits on <code>done</code>.</p><p>Why the reader and not the writer? The reader finishing means the server closed its side, nothing more is coming. The writer might finish, user typed the sentinel, while the server is still sending the last echo back. Waiting on the reader catches the true end of the conversation.</p><p>Here&#8217;s what it looks like running:</p><p><strong>Client:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">go run client/main.go
connected to server
hi
echo: hi
by
echo: by
bye
echo: bye
EOF</code></pre></div><p><strong>Server:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">go run server/main.go
[127.0.0.1:50472]: connected
[127.0.0.1:50472]: hi
[127.0.0.1:50472]: by
[127.0.0.1:50472]: bye
[127.0.0.1:50472]: disconnected</code></pre></div><p>Each line the client types comes back prefixed with <code>echo:</code>. When I type <code>EOF</code>, the <code>WrappedReader</code> returns <code>io.EOF</code>, the writer goroutine calls <code>CloseWrite()</code>, the server sees the connection close, logs &#8220;disconnected&#8221;, and shuts down its side. The full teardown plays out exactly as the diagrams show.</p><div><hr></div><h2>CloseWrite Is Not Close</h2><p>When the writer goroutine finishes, I don&#8217;t want to close the whole connection. The reader is still waiting for the server&#8217;s final echoes.</p><p><code>conn.Close()</code> would close both directions immediately. The reader goroutine would break mid-flight.</p><p><code>conn.(*net.TCPConn).CloseWrite()</code> closes only the write side. Underneath it calls <code>shutdown(fd, SHUT_WR)</code>. The kernel sends a FIN to the server signalling &#8220;I&#8217;m done sending&#8221;, but <code>fd=3</code> stays open for reading. The pollDesc entry stays registered with epoll. The <code>g_client</code> reader goroutine keeps running.</p><p>I had to look this up to understand the type assertion. <code>net.Conn</code> is an interface. It doesn&#8217;t expose <code>CloseWrite()</code>. We have to assert to <code>*net.TCPConn</code> first:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">conn.(*net.TCPConn).CloseWrite()</code></pre></div><p>A half-close. Each direction closed independently, when each side was ready.</p><div><hr></div><h2>The Sentinel Protocol and Its Limits</h2><p>To signal end-of-input from the terminal without Ctrl+D, I built a <code>WrappedReader</code> around <code>os.Stdin</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type WrappedReader struct {
    r io.Reader
}

func (wr WrappedReader) Read(p []byte) (int, error) {
    n, err := wr.r.Read(p)
    s := string(p[:n])
    if s == "EOF\n" {
        return 0, io.EOF
    }
    return n, err
}</code></pre></div><p>If it reads <code>"EOF\n"</code>, it returns <code>io.EOF</code> to the caller instead of passing the bytes through.</p><p>Three things I got wrong building this before getting it right.</p><p><code>string(p)</code> not <code>string(p[:n])</code>. <code>p</code> is a 32KB buffer. Only <code>n</code> bytes are valid. The rest is garbage. Reading the full <code>p</code> gives you junk after the actual input.</p><p>Terminal stdin is line-buffered. You get <code>"EOF\n"</code> as one chunk, not byte by byte. So the comparison works cleanly.</p><p><code>io.Copy</code> calls <code>Read</code> in a loop. Returning <code>io.EOF</code> is the signal to stop. Not an error, just done.</p><p>It worked. And it immediately showed me its own problem.</p><p>What if the user actually wants to send the literal string <code>"EOF"</code>? The protocol misinterprets it as a control signal. What if the payload is binary, a JPEG, a serialised struct? Any byte sequence could accidentally match a text sentinel.</p><p>This is the problem real protocols solve with framing.</p><p>HTTP uses <code>Content-Length</code> or <code>Transfer-Encoding: chunked</code>. Redis&#8217;s RESP uses type-tagged length-prefixed messages, <code>$6\r\nfoobar\r\n</code>. gRPC uses binary length-prefixed frames over HTTP/2.</p><p>The sentinel approach breaks the moment the payload itself contains the sentinel string. Framing works on everything.</p><div><hr></div><h2>What Happens to the Resources</h2><p>The teardown is where I saw the full picture of what a connection actually costs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0gZl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0gZl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 424w, https://substackcdn.com/image/fetch/$s_!0gZl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 848w, https://substackcdn.com/image/fetch/$s_!0gZl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 1272w, https://substackcdn.com/image/fetch/$s_!0gZl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0gZl!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png" width="1200" height="702.1978021978022" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:852,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:490042,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0gZl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 424w, https://substackcdn.com/image/fetch/$s_!0gZl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 848w, https://substackcdn.com/image/fetch/$s_!0gZl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 1272w, https://substackcdn.com/image/fetch/$s_!0gZl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f36c674-893f-4bc6-811d-6f8fd961f77a_3633x2126.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When the writer goroutine finishes and <code>CloseWrite()</code> fires, <code>shutdown(fd=3, SHUT_WR)</code> is called. The kernel flushes and closes the send buffer. The FIN packet goes out. The socket transitions to <code>FIN_WAIT_1</code>. But <code>fd=3</code> is still open. The <code>rcv_buff</code> is still alive. The reader goroutine is still parked, waiting.</p><p><code>fd=3</code> is still registered with epoll. Still in the interest list. The netpoller is still watching it.</p><p>On the server side, the FIN arrives at the NIC. The kernel places an EOF marker into <code>rcv_buff</code> of <code>fd=10</code>. The kernel marks <code>fd=10</code> as ready in epoll, EOF counts as readable. </p><p>The netpoller unparks <code>g_conn_1</code>. <code>conn.Read</code> returns the EOF. <code>sc.Scan()</code> returns false. The for loop exits.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!H07A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H07A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 424w, https://substackcdn.com/image/fetch/$s_!H07A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 848w, https://substackcdn.com/image/fetch/$s_!H07A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 1272w, https://substackcdn.com/image/fetch/$s_!H07A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H07A!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png" width="1200" height="910.7142857142857" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1105,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:682238,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!H07A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 424w, https://substackcdn.com/image/fetch/$s_!H07A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 848w, https://substackcdn.com/image/fetch/$s_!H07A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 1272w, https://substackcdn.com/image/fetch/$s_!H07A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F767c36e5-9b70-4aa0-b447-e7a6fe72fad6_3591x2725.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>defer conn.Close()</code> fires on the server. Three things happen in order.</p><ul><li><p><code>epoll_ctl(DEL, fd=10)</code> removes <code>fd=10</code> from the epoll interest list. </p></li></ul><ul><li><p>The pollDesc entry for <code>g_conn_1</code> is cleared. </p></li></ul><ul><li><p>The netpoller is no longer watching this fd.</p></li></ul><p><code>close(fd=10)</code> syscall. The kernel decrements the reference count on the socket. If it hits zero, <code>fd=10</code> is freed from the server&#8217;s process fd table, and the kernel sends a FIN to the client.</p><p>The server socket transitions to <code>LAST_ACK</code>, waiting for the client&#8217;s final ACK.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!C5Ny!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!C5Ny!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 424w, https://substackcdn.com/image/fetch/$s_!C5Ny!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 848w, https://substackcdn.com/image/fetch/$s_!C5Ny!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 1272w, https://substackcdn.com/image/fetch/$s_!C5Ny!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!C5Ny!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png" width="1200" height="1278.2967032967033" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/881b2ead-545f-4d02-b752-30e355186494_3418x3642.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1551,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:689505,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!C5Ny!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 424w, https://substackcdn.com/image/fetch/$s_!C5Ny!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 848w, https://substackcdn.com/image/fetch/$s_!C5Ny!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 1272w, https://substackcdn.com/image/fetch/$s_!C5Ny!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F881b2ead-545f-4d02-b752-30e355186494_3418x3642.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The client&#8217;s reader goroutine sees the server&#8217;s FIN &#8212; the kernel placed an EOF marker in <code>rcv_buff</code> of <code>fd=3</code>, epoll fired, the netpoller unparked <code>g_client</code>. <code>io.Copy(os.Stdout, conn)</code> returns. <code>done &lt;- 1</code>. Main unblocks. <code>conn.Close()</code> is called on the client side.</p><p>On the client, <code>conn.Close()</code> triggers its own three-step sequence. <code>epoll_ctl(DEL, fd=3)</code> removes <code>fd=3</code> from the epoll interest list. The pollDesc entry for <code>g_client</code> is cleared. <code>close(fd=3)</code> frees <code>fd=3</code> from the client&#8217;s process fd table &#8212; the Go process can reuse that integer for a new connection immediately. As part of <code>close(fd=3)</code>, the kernel sends the final ACK to the server and transitions the client socket to <code>TIME_WAIT</code>.</p><p>The server receives the final ACK. <code>cli:tcp_sock</code> transitions to <code>CLOSED</code>. The kernel frees everything on the server side &#8212; the <code>tcp_sock</code> structure, <code>rcv_buff</code>, <code>send_buff</code>, all TCP state &#8212; sequence numbers, timers, everything.</p><p>On the client side, the socket lingers in <code>TIME_WAIT</code> for up to 120 seconds (2 * MSL, Maximum Segment Lifetime). The <code>rcv_buff</code> stays alive through this period &#8212; the kernel needs it in case the server retransmits its FIN. Only after 2*MSL expires does the kernel free the client&#8217;s <code>tcp_sock</code>, <code>rcv_buff</code>, and all remaining state.</p><p>Two reasons TIME_WAIT exists. If the final ACK got lost, the server will retransmit its FIN &#8212; the client needs to still be alive to respond. And if the same 4-tuple gets reused for a new connection too quickly, a stale packet from the old connection could arrive and corrupt it. <code>TIME_WAIT</code> prevents both.</p><p>The netpoller&#8217;s involvement ends at <code>conn.Close()</code>. Everything after &#8212; <code>LAST_ACK</code>, <code>TIME_WAIT</code>, final memory free &#8212; is the kernel&#8217;s TCP state machine running on its own.</p><div><hr></div><h2>The Resource Lifecycle, in Full</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!buIB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!buIB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 424w, https://substackcdn.com/image/fetch/$s_!buIB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 848w, https://substackcdn.com/image/fetch/$s_!buIB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 1272w, https://substackcdn.com/image/fetch/$s_!buIB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!buIB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png" width="1180" height="735" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:735,&quot;width&quot;:1180,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94252,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/196290511?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!buIB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 424w, https://substackcdn.com/image/fetch/$s_!buIB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 848w, https://substackcdn.com/image/fetch/$s_!buIB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 1272w, https://substackcdn.com/image/fetch/$s_!buIB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb944e6ed-52ce-4e56-b263-8617a1a44806_1180x735.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The fd dies at <code>conn.Close()</code>. The pollDesc dies with it. On the server side, <code>conn.Close()</code> sends the FIN, the client&#8217;s final ACK arrives, and the kernel frees everything &#8212; <code>tcp_sock</code>, both buffers, all TCP state &#8212; when it reaches CLOSED.</p><p>On the client side it&#8217;s different. <code>conn.Close()</code> fires the final ACK as part of <code>close(fd=3)</code>, and the socket transitions to TIME_WAIT. But the <code>rcv_buff</code> stays alive through TIME_WAIT &#8212; the kernel needs it in case the server retransmits its FIN. The client has to be able to receive and respond. Only after 2*MSL expires does the kernel free the <code>tcp_sock</code>, <code>rcv_buff</code>, and all remaining state.</p><p>The fd and pollDesc die immediately at <code>conn.Close()</code>. The kernel socket outlives them both.</p><p>Every row in that table is real kernel work. Every connection teardown goes through it.</p><div><hr></div><h2>This Is Why Connection Pools Exist</h2><p>Look at that table again. Every connection teardown fires <code>epoll_ctl(DEL)</code>, clears the pollDesc, closes the fd, sends a FIN, waits for ACK, frees the socket structure, frees both buffers, waits out TIME_WAIT. And that&#8217;s just the teardown. The setup side is just as expensive &#8212; <code>socket()</code>, <code>bind()</code>, <code>connect()</code>, the three-way handshake, buffer allocation, pollDesc registration.</p><p>Every new connection pays that cost twice. Once to set up, once to tear down.</p><p>In the teardown diagrams above, I marked every resource cleanup event in green &#8212; <code>epoll_ctl(DEL, fd=10)</code>, the pollDesc entry being cleared, the fd freed from the process table, the final kernel free of <code>tcp_sock</code>, <code>rcv_buff</code>, <code>send_buff</code>, and all TCP state. Each green box is real kernel work that happens on every single connection close.</p><p>Connection pooling is the realisation that most of that work is unnecessary if the connection can be reused. Instead of tearing a connection down after each request, a pool keeps it alive and hands it back for the next one. None of those green boxes fire. The <code>tcp_sock</code> stays in kernel memory. The buffers stay allocated. The <code>pollDesc</code> entry stays registered with epoll. The next request skips the handshake entirely and goes straight to writing bytes into an already-established connection.</p><p>The netpoller makes Go capable of holding thousands of those alive connections open simultaneously without wasting OS threads. But the connections themselves still cost kernel memory, fd table entries, and buffer allocations. That cost doesn&#8217;t disappear just because goroutines are cheap.</p><p>Go&#8217;s netpoller solves the thread problem. Connection pools solve the connection cost problem. They operate at different layers and both matter.</p><p>Pool them. Reuse them. The teardown is expensive.</p><div><hr></div><h2>What&#8217;s Still Open</h2><p>I now understand what happens when you call <code>net.Listen</code> and <code>net.Dial</code>. The kernel owns more of the process than I realised. The handshake, the queues, the buffers, the teardown, most of it runs without our code ever being called.</p><p>Go&#8217;s contribution is making the interface to all of that feel sequential. <code>Accept()</code> looks blocking. <code>Read()</code> looks blocking. Neither holds a thread while waiting. The netpoller and epoll handle the gap between &#8220;nothing ready&#8221; and &#8220;resume here.&#8221;</p><p>What I still don&#8217;t understand well enough: what happens when thousands of connections are open simultaneously and I don&#8217;t want to pay the handshake cost every time. How connection pools work. How HTTP&#8217;s request-response framing sits on top of exactly this primitive. How the send and receive buffers interact with TCP&#8217;s flow control when one side is slow.</p><p>That&#8217;s where this goes next.</p><div><hr></div><p><em>The diagrams in this article are from my Excalidraw canvas where I mapped the full TCP connection lifecycle while building this. The full canvas is in the <a href="https://github.com/vishal2098govind/http-deep-dive/blob/main/cmd/client-server/tcp-connection-deep-dive.excalidraw">repo here</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[I Thought Logging Was One Line Until I Saw the Cost of Serialization Through Buffer Pooling]]></title><description><![CDATA[Concurrent writes corrupt logs. Mutex bottlenecks serialize work. Buffer pooling reveals what serialization actually costs]]></description><link>https://logs.craftedbyvishal.dev/p/i-thought-logging-was-one-line-until</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/i-thought-logging-was-one-line-until</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Thu, 23 Apr 2026 04:00:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!GrBl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What Logging Actually Costs</h1><p>I used to think logging was this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">log.Info("request ended")</code></pre></div><p>One line. Done.</p><p>Turns out logging under high concurrency is where I finally understood what serialization actually costs. I&#8217;d seen memory spikes in CloudWatch before when traffic spiked 1,000+ requests/min on a single EC2 instance, memory usage climbing. Understanding how logging works under concurrency made allocation pressure visible.</p><div><hr></div><h2>What a log write actually is</h2><p>When you call:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">log.Info(ctx, "Request ended", "duration", "8.2s", "status", 200)</code></pre></div><p>Three things happen.</p><p><strong>Formatting.</strong> Individual fields get assembled into a byte sequence:</p><pre><code><code>"duration" + "8.2s" + "status" + 200
        &#8595;
{"time":"...","level":"INFO","msg":"Request ended","duration":"8.2s","status":200}</code></code></pre><p>This assembled byte sequence lives in a scratch buffer in memory while being built.</p><p><strong>Write.</strong> The formatted bytes get written to fd=1 (stdout):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">w.Write(buf)  // syscall &#8594; kernel &#8594; fd=1 &#8594; wherever stdout points</code></pre></div><p><strong>Discard.</strong> The scratch buffer is done, goes out of scope.</p><p>Simple enough for one goroutine. Now add 1000 concurrent request handlers all logging simultaneously.</p><div><hr></div><h2>The concurrent write problem</h2><p>Two goroutines writing to fd=1 simultaneously without coordination:</p><pre><code><code>goroutine A: {"level":"INFO","msg":"Request en
goroutine B: {"level":"ERROR","msg":"upload failed"}
goroutine A: ded","duration":"8.2s"}</code></code></pre><p>What lands on disk:</p><pre><code><code>{"level":"INFO","msg":"Request en{"level":"ERROR","msg":"upload failed"}ded","duration":"8.2s"}</code></code></pre><p>Not valid JSON. Log parsers break. Loki can&#8217;t index fields. Grafana shows garbage. A real production incident becomes unreadable at the worst possible moment.</p><p>The fix is a mutex around the write.</p><p><code>slog</code>&#8216;s <code>JSONHandler</code> does exactly this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type JSONHandler struct {
    mu *sync.Mutex
    w  io.Writer
}

func (h *JSONHandler) Handle(ctx context.Context, r Record) error {
    h.mu.Lock()
    defer h.mu.Unlock()
    // format entire record into internal buffer
    // write complete JSON line to w
}</code></pre></div><p>One goroutine holds the mutex, formats and writes, releases. Others wait. Each log line lands as a complete JSON object, no interleaving.</p><div><hr></div><h2>The mutex bottleneck</h2><p>The mutex solves correctness. But it covers the entire format+write operation:</p><pre><code><code>[goroutine A holds mutex: building JSON string... writing... done]
[goroutine B waits                                               ]
[goroutine C waits                                               ]</code></code></pre><p>Under 1000 concurrent goroutines, 999 are waiting while 1 formats. The expensive part, assembling the JSON string, is serialized even though it could be done in parallel. Each goroutine is working with its own data, there&#8217;s no reason they can&#8217;t format simultaneously.</p><p>The insight: formatting doesn&#8217;t need shared state. Only the write to fd=1 needs coordination.</p><pre><code><code>slog:    [mutex: format + write]          &#8592; mutex too wide
zerolog: format (no mutex) &#8594; [mutex: write only]  &#8592; mutex as narrow as possible</code></code></pre><div><hr></div><h2>zerolog&#8217;s approach, format outside the lock</h2><p>zerolog moves formatting outside the mutex by giving each goroutine its own scratch buffer to format into:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">// goroutine A                    // goroutine B
buf_A := pool.Get()               buf_B := pool.Get()
buf_A.Write(`{"level":"INFO"...`) buf_B.Write(`{"level":"ERROR"...`)
// both formatting in parallel, no contention

mutex.Lock()                      // B waits here
w.Write(buf_A)
mutex.Unlock()
                                  mutex.Lock()
                                  w.Write(buf_B)
                                  mutex.Unlock()</code></pre></div><p>Formatting happens in parallel. The mutex window shrinks to just the write syscall, microseconds instead of milliseconds.</p><p>But where do the scratch buffers come from?</p><p>The naive approach: allocate a fresh <code>[]byte</code> for every log call. Under 1000 requests/sec, that&#8217;s 1000 allocations/sec just for log buffers, continuous GC pressure. This is where <code>sync.Pool</code> comes in.</p><div><hr></div><h2><code>sync.Pool</code>: object pooling for scratch buffers</h2><p><code>sync.Pool</code> is a pool of reusable objects. Instead of allocating and discarding:</p><pre><code><code>allocate &#8594; use &#8594; discard &#8594; GC collects &#8594; allocate &#8594; use &#8594; discard &#8594; GC collects ...</code></code></pre><p>You reuse:</p><pre><code><code>allocate once &#8594; use &#8594; reset &#8594; return to pool &#8594; use &#8594; reset &#8594; return to pool ...</code></code></pre><p>Basic usage:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">var pool = &amp;sync.Pool{
    New: func() any {
        return bytes.NewBuffer(make([]byte, 0, 1024))
    },
}

buf := pool.Get().(*bytes.Buffer)   // checkout: reuse existing or allocate new
buf.WriteString(`{"level":"INFO"}`) // format into it
// ...write to fd=1...
buf.Reset()                          // clear contents, keep underlying memory
pool.Put(buf)                        // checkin: available for next caller</code></pre></div><p>The key: <code>buf.Reset()</code> clears the contents but keeps the allocated <code>[]byte</code> underneath. The memory is reused, not garbage collected and reallocated.</p><p><strong>A catch:</strong> buffer sizes vary. One log message might need 500 bytes, another 5KB. But <code>bytes.Buffer</code> grows dynamically, and keeps that capacity after <code>Reset()</code>:</p><pre><code><code>buf := pool.Get().(*bytes.Buffer)  // capacity: 1024 (initial)
buf.WriteString(veryLongJSON)      // content: 8KB &#8594; buffer grows to 8KB capacity
buf.Reset()                        // content cleared, capacity: still 8KB
pool.Put(buf)                      // 8KB-capacity buffer goes back to pool
</code></code></pre><p><code>Reset()</code> zeroes the length but doesn&#8217;t touch the underlying <code>[]byte</code> array. The slice capacity stays. So the pool naturally holds buffers at the high-watermark of what&#8217;s been written through them.</p><p>After a warm-up period, pool buffers converge to the size of typical log lines, not the initial 1024 you allocated in <code>New</code>. Each buffer has organically grown to fit real usage. Occasional large entries cause a one-time growth, then that larger buffer circulates.</p><p>The trade-off: an unusually large log line (say, a 500KB debug dump) causes one buffer to grow to 500KB. That buffer now lives in the pool permanently, until GC collects it via the victim cycle. You&#8217;re holding 500KB of RAM for a buffer that 99% of requests will use 2KB of.</p><p>But you&#8217;re still orders of magnitude better than allocating fresh on every call. Pooling doesn&#8217;t assume uniform buffer sizes. It assumes that buffers, once grown to fit real usage, will keep fitting real usage. The growth cost is paid once. The reuse benefit is paid on every subsequent call.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GrBl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GrBl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 424w, https://substackcdn.com/image/fetch/$s_!GrBl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 848w, https://substackcdn.com/image/fetch/$s_!GrBl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 1272w, https://substackcdn.com/image/fetch/$s_!GrBl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GrBl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png" width="1156" height="384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:384,&quot;width&quot;:1156,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:109020,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/194721008?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GrBl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 424w, https://substackcdn.com/image/fetch/$s_!GrBl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 848w, https://substackcdn.com/image/fetch/$s_!GrBl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 1272w, https://substackcdn.com/image/fetch/$s_!GrBl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F532024e7-df3f-4c05-b0c7-e51d79a6c3ef_1156x384.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is what allocation pressure looks like. Concurrent request handlers each allocating scratch buffers. Without pooling, every goroutine allocates fresh. Under high traffic, those allocations pile up faster than GC can collect them. Memory climbs. The spike isn&#8217;t a leak. It&#8217;s just allocation pressure under concurrency.</p><p>This is what <code>sync.Pool</code> prevents.</p><div><hr></div><h2>Why <code>sync.Pool</code> is lock-free in the common case</h2><p>A naive pool would use a mutex-protected slice. Under high concurrency, goroutines would contend on that mutex, no better than what we started with.</p><p><code>sync.Pool</code> is smarter. It exploits Go&#8217;s scheduler structure.</p><p>Go&#8217;s scheduler has P processors (one per CPU core by default). Every goroutine runs on a P. <code>sync.Pool</code> gives each P its own local slot:</p><pre><code><code>Pool
  local[0] &#8594; buf   &#8592; P0's slot
  local[1] &#8594; buf   &#8592; P1's slot
  local[2] &#8594; nil   &#8592; P2's slot, empty
  local[3] &#8594; buf   &#8592; P3's slot
</code></code></pre><p>When a goroutine calls <code>pool.Get()</code>:</p><ol><li><p>Which P am I on? Say P0.</p></li><li><p>Is <code>local[0]</code> non-nil? Yes &#8594; take it. Done. <strong>No lock needed.</strong></p></li><li><p>If nil &#8594; check other P&#8217;s slots (needs lock) &#8594; if all empty &#8594; call <code>New</code></p></li></ol><p>The common case (P has an object in its slot) is lock-free. Only the uncommon case (P&#8217;s slot is empty, need to steal from another P) requires a lock.</p><p>The goroutine stays on P0 the entire time. When P0&#8217;s slot is empty, the goroutine (still on P0) reaches across and takes an object from another P&#8217;s slot. The object moves, the goroutine doesn&#8217;t.</p><p>This is called <strong>work stealing</strong>, the same pattern the Go scheduler uses, but here we&#8217;re stealing objects, not goroutines.</p><div><hr></div><h2>The GC interaction: victim cache</h2><p>Here&#8217;s where it gets interesting.</p><p><code>sync.Pool</code> is not a cache. Objects in the pool can disappear at any GC cycle. But there&#8217;s a grace period.</p><p>When GC runs, <code>sync.Pool</code> doesn&#8217;t immediately drop everything. It moves the current pool contents into a <strong>victim cache</strong>, a secondary holding area. The victim cache survives for exactly one more GC cycle.</p><pre><code><code>GC #1: current pool &#8594; victim cache
       pool is now empty

During the next GC interval:
  - Get() checks current pool first
  - If empty, checks victim cache
  - If both empty, calls New()

GC #2: victim cache is cleared
       (objects from GC #1 are finally eligible for collection)
</code></code></pre><p>Why does this matter?</p><p>After a traffic spike, allocation rate drops. GC runs. Without the victim cache, the pool would be empty immediately, and the next request would allocate fresh buffers even though the spike just ended.</p><p>The victim cache gives the system <strong>two GC cycles</strong> to smooth the transition. If traffic stays low, objects eventually get collected. If traffic spikes again before the second GC, the victim cache prevents re-allocation.</p><div><hr></div><h2>The pattern appears everywhere</h2><p>Once I understood why <code>sync.Pool</code> exists for logging, I started seeing it everywhere.</p><h3>HTTP request/response body buffers</h3><p>When Go&#8217;s <code>net/http</code> receives a request, it reads raw TCP bytes into a buffer before parsing headers, body, etc. Thousands of concurrent connections = thousands of concurrent buffer allocations without pooling. Go&#8217;s HTTP server uses <code>sync.Pool</code> internally for exactly this.</p><pre><code><code>TCP socket &#8594; pool buffer &#8594; HTTP parser &#8594; request struct
                &#8593; pooled</code></code></pre><h3>JSON encode/decode scratch buffers</h3><p><code>json.Marshal</code> builds a <code>[]byte</code> while encoding your struct to JSON. <code>json.Unmarshal</code> needs scratch space for tokenizing the input. Both operations are short-lived and happen on every API request.</p><pre><code><code>Go struct &#8594; scratch buffer (JSON bytes being built) &#8594; write to TCP socket
                &#8593; pooled</code></code></pre><p>The buffer contains the in-progress JSON string, exists only for the duration of the marshal call, then done. Libraries like <code>jsoniter</code> and <code>easyjson</code> use pools aggressively for this reason.</p><h3>Database query result buffers</h3><p>The database driver reads raw bytes from the TCP connection (the database protocol wire format) into a scratch buffer, then deserializes into your Go structs. The buffer is the temporary home for raw wire data between &#8220;arrived from network&#8221; and &#8220;parsed into application types.&#8221;</p><pre><code><code>TCP bytes (DB wire protocol) &#8594; pool buffer &#8594; deserializer &#8594; your Go struct
                                    &#8593; pooled</code></code></pre><p>Under concurrent queries, each goroutine needs its own buffer, pool provides them without per-query allocation cost.</p><h3>Protobuf marshalling buffers</h3><p>Same as JSON, protobuf encoding builds a byte sequence before writing to the network. <code>protoc</code>-generated Go code uses pools internally for the scratch buffer.</p><h3>The general pattern</h3><p>Any time you see:</p><ul><li><p>A scratch buffer allocated at the start of an operation</p></li><li><p>Used temporarily to hold intermediate bytes</p></li><li><p>Discarded when the operation completes</p></li><li><p>Under concurrent load</p></li></ul><p>That&#8217;s a candidate for <code>sync.Pool</code>.</p><p>The smell in code: <code>make([]byte, ...)</code> or <code>new(SomeStruct)</code> inside a hot path that runs thousands of times per second.</p><div><hr></div><h2>Object pooling vs connection pooling</h2><p>Connection pooling reuses TCP connections instead of creating new ones per request. Creating a TCP connection is expensive: 3-way handshake, TLS negotiation, kernel buffer allocation.</p><p>Buffer/object pooling reuses memory allocations instead of allocating per operation. Creating a buffer is cheap individually but expensive at scale: GC pressure, allocation spikes, increased latency variance.</p><p>Same underlying idea: <strong>expensive-to-create resource, reuse instead of recreate.</strong> Different resources, same pattern.</p><pre><code><code>                    Connection Pool              Buffer Pool
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
Resource            TCP connection               Memory allocation
Expense avoided     Handshake + kernel setup     GC pressure + allocation
Go primitive        http.Transport               sync.Pool
Lifecycle           Long-lived                   Ephemeral (reset per use)
Pool size           Bounded (MaxIdleConns)       Unbounded, GC-managed</code></code></pre><div><hr></div><h2>The hidden cost of serialization</h2><p>Understanding buffer pooling leads to a bigger realization: <strong>serialization is not free, and we introduce it constantly without thinking about it.</strong></p><p>Every log write serializes fields into JSON bytes. We studied this carefully, scratch buffers, GC pressure, mutex contention. But the exact same cost exists at every API boundary in a system:</p><pre><code><code>Service A
    &#8595;
marshal to JSON/protobuf
(scratch buffer, GC pressure, CPU cycles building bytes)
    &#8595;
network
    &#8595;
unmarshal
(scratch buffer, GC pressure, CPU cycles parsing bytes)
    &#8595;
Service B</code></code></pre><p>Every microservice call pays this twice. Every database query pays it. Every cache read pays it. Every message queue publish/consume pays it.</p><h3>What serialization actually costs</h3><p>At the code level it looks like one line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">json.Marshal(myStruct)</code></pre></div><p>What&#8217;s actually happening:</p><ul><li><p>Allocate a scratch buffer</p></li><li><p>Reflect over every field of the struct</p></li><li><p>Convert each field to its string/byte representation</p></li><li><p>Write delimiters, quotes, brackets</p></li><li><p>Return the completed byte sequence</p></li><li><p>GC eventually collects the scratch buffer</p></li></ul><p>And then on the receiving end, unmarshal does the reverse, tokenize the bytes, allocate new struct fields, parse each value back into its Go type.</p><p>Under high concurrency, this is happening thousands of times per second across every service boundary.</p><h3>The invisible tax of microservices</h3><p>In a monolith, a function call has zero serialization cost:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">result := orderService.GetOrder(id)  // in-process, just a pointer</code></pre></div><p>Split that into two services:</p><pre><code><code>Client
    &#8595;
marshal request
    &#8595;
HTTP
    &#8595;
unmarshal request
    &#8595;
[Service processes]
    &#8595;
marshal response
    &#8595;
HTTP
    &#8595;
unmarshal response
    &#8595;
Client receives result</code></code></pre><p>Four serialization operations for what was zero. Every service boundary you add multiplies this cost across every request that crosses it.</p><p>Teams have measured 30-40% of total request latency in some microservice architectures being pure serialization overhead, not business logic, not database queries, not network RTT. Just converting data between bytes and structs.</p><h3>Why we don&#8217;t notice</h3><p>Frameworks hide it. <code>gin.ShouldBindJSON(&amp;req)</code> looks like one line. gRPC stubs look like function calls. The cost is real but invisible until you profile under load and see serialization dominating your flame graph.</p><h3>The design implication</h3><p>Every time you say &#8220;let&#8217;s add an API&#8221; or &#8220;let&#8217;s split this service,&#8221; you&#8217;re adding a serialization boundary. That&#8217;s sometimes the right call, isolation, independent deployments, team ownership. But it&#8217;s a <strong>cost</strong>, not just an architectural choice.</p><p>Questions worth asking before adding an API boundary:</p><ul><li><p>Can this be an in-process call instead?</p></li><li><p>If it must be remote, can we use a binary format (protobuf) instead of JSON?</p></li><li><p>Can we batch calls to amortize the serialization cost?</p></li><li><p>Is the data structure designed to serialize efficiently (flat vs deeply nested)?</p></li></ul><h3>Protobuf vs JSON: not just syntax</h3><p>This is why protobuf exists. JSON serialization uses reflection and string manipulation. Protobuf uses generated code with direct field access no reflection, smaller wire format, faster parse. The trade-off: schema must be defined upfront, less human-readable.</p><p>At high volume, this matters:</p><pre><code><code>JSON:     ~500ns to marshal a typical request struct
Protobuf: ~100ns for the same struct</code></code></pre><p>5x difference. Across millions of requests per day, that&#8217;s real CPU time.</p><h3>The logging connection</h3><p>This is what made logging non-trivial: it&#8217;s serialization on every request, under concurrency, in the hot path. The solutions, zerolog, sync.Pool, narrow mutexes, are the same solutions high-performance serialization libraries use everywhere.</p><p>Logging just made the problem visible because it happens on <strong>every single request</strong>, not just at service boundaries.</p><div><hr></div><h2>What I learned</h2><p>A log write is format + write. Format is parallelizable, write must be serialized. Putting both under the same mutex is the conservative but suboptimal choice.</p><p><code>sync.Pool</code> is for ephemeral scratch space. Short-lived, frequently allocated, cheap to reset. Not for objects with meaningful state.</p><p>Pool size is dynamic. Grows under spikes, shrinks after two GC cycles. No manual sizing.</p><p>Per-P slots make Get/Put lock-free in the common case. The GC interaction (victim cache) gives a two-cycle grace period to smooth post-GC allocation spikes.</p><p>Buffer pooling and connection pooling are the same idea. Reuse expensive-to-create resources instead of creating them fresh per operation.</p><p>The pattern appears everywhere in high-performance Go. <code>net/http</code>, <code>encoding/json</code>, database drivers, protobuf  all use pooling internally.</p><p>Serialization is not free, and we introduce it constantly. Every API boundary pays serialization cost twice (marshal + unmarshal). Every microservice split multiplies this across every request that crosses the boundary. In-process function calls have zero serialization cost.</p><p>&#8220;Let&#8217;s add an API&#8221; is a serialization decision. Before adding a service boundary, ask: can this be in-process? If remote is necessary, use binary formats (protobuf) over text (JSON), batch calls, and design flat data structures. 30-40% of request latency in some microservice architectures is pure serialization overhead, not business logic, not network RTT.</p><p>Logging made serialization visible because it&#8217;s on every single request. The solutions (zerolog, sync.Pool, narrow mutexes) are the same patterns high-performance serialization libraries use everywhere. Logging is just the place where the cost became impossible to ignore.</p><div><hr></div>]]></content:encoded></item><item><title><![CDATA[HTTP Deep Dive - HTTP File Upload: Memory Flow, SSE Progress, and Token Bucket Rate Limiting]]></title><description><![CDATA[Understanding where upload data lives in RAM on it's way to disk, how HTTP SSE streams work, and how can a token bucket control data flow at the Go's io.Copy layer]]></description><link>https://logs.craftedbyvishal.dev/p/http-deep-dive-http-file-upload-memory</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/http-deep-dive-http-file-upload-memory</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Sat, 18 Apr 2026 13:16:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Yddc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built a file upload service to understand where data lives during an 8GB upload. Not &#8220;in memory&#8221; as a vague concept. Where in RAM? How much? For how long?</p><p>The upload finished too fast to observe. An 838-byte test file completed in under a millisecond. To actually see progress events stream back to the client, I needed to slow it down.</p><p>That led to building a token bucket rate limiter from scratch. Not because the service needed throttling, but because understanding how to control data flow at the io.Reader layer turned out to be the clearest way to understand streaming itself.</p><p>This is Part 1 of HTTP Protocol Deep Dives. Each article covers one prototype built from scratch to understand a specific HTTP concept at the wire level.</p><div><hr></div><h2>What Gets Built</h2><p>Three endpoints:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">POST /upload/initiate         # &#8594; registers upload, returns {upload_id, upload_url}
PUT  /upload/{uploadid}       # &#8594; receives file, streams to disk
GET  /upload/progress/{uploadid}  # &#8594; SSE stream of upload progress</code></pre></div><p>No S3 pre-signed URLs. No message queues. The goal is to understand the HTTP machinery directly, not abstract it away.</p><p>The upload endpoint accepts <code>multipart/form-data</code> and streams the file to disk using <code>io.Copy</code>. The progress endpoint opens a Server-Sent Events stream and polls upload state every 200ms. Two separate HTTP connections for one upload.</p><div><hr></div><h2>Where Data Lives</h2><p>Before walking through the implementation, here&#8217;s where the data actually sits at each stage.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uTbi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uTbi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 424w, https://substackcdn.com/image/fetch/$s_!uTbi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 848w, https://substackcdn.com/image/fetch/$s_!uTbi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 1272w, https://substackcdn.com/image/fetch/$s_!uTbi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uTbi!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png" width="1200" height="614.8351648351648" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:746,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:2052405,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/194604916?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uTbi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 424w, https://substackcdn.com/image/fetch/$s_!uTbi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 848w, https://substackcdn.com/image/fetch/$s_!uTbi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 1272w, https://substackcdn.com/image/fetch/$s_!uTbi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7086ea4c-24a5-4507-afbe-48151f0bf7e1_3003x1538.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For an 8GB upload:</p><ul><li><p>Socket buffer: ~128KB (kernel-managed)</p></li><li><p>io.Copy buffer: 32KB (user-space, reused)</p></li><li><p>Total RAM: ~160KB</p></li></ul><p>The rest is either in transit on the network, already on disk, or waiting in the OS page cache to flush.</p><blockquote><p>Streaming means never accumulating.</p></blockquote><p>This is what makes <code>io.Copy</code> work. It allocates a 32KB buffer once and reuses it in a loop. The buffer size doesn&#8217;t grow with file size.</p><h3>The <code>io.Reader</code> contract</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type Reader interface {
    Read(p []byte) (n int, err error)
}</code></pre></div><p>The caller allocates the buffer <code>p</code>. The reader fills it. Returns <code>n</code>, the number of bytes written. The caller only looks at <code>p[:n]</code>, never <code>p[:]</code>.</p><p><code>io.Copy</code> does this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">buf := make([]byte, 32*1024)  // allocated once
for {
    nr, er := src.Read(buf)
    if nr &gt; 0 {
        nw, ew := dst.Write(buf[0:nr])
    }
    if er == io.EOF { break }
}</code></pre></div><p>Every <code>src.Read</code> asks &#8220;give me up to 32KB.&#8221; The reader returns however many bytes are available, could be 32KB, could be 1 byte, could be 0 with an error. The caller handles all three.</p><div><hr></div><h2>The Upload Flow</h2><p>Here&#8217;s what happens when a file moves through the system.</p><h3>Step 1: Initiate</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">POST /upload/initiate</code></pre></div><p>Curl command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">curl -i -X POST http://localhost:8080/upload/initiate</code></pre></div><p>Server generates an upload ID, creates an entry in the progress store:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "upload_id": "0f033e91-5317-4476-bc89-4a64bc3354d0",
  "upload_url": "/upload/0f033e91-5317-4476-bc89-4a64bc3354d0"
}</code></pre></div><p>The code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func (u *UploadAPI) InitiateUpload(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
    id := uuid.NewString()
    u.ups.SetProgress(id, uploadprogress.Progress{})
    apis.WriteJson(w, http.StatusOK, apis.ApiResponse{
        Data: struct {
            UploadId  string `json:"upload_id"`
            UploadUrl string `json:"upload_url"`
        }{
            UploadId:  id,
            UploadUrl: fmt.Sprintf("/upload/%v", id),
        },
    })
    return nil
}</code></pre></div><p>The progress store entry exists before either connection starts. This solves the race, the SSE handler can connect and start polling immediately without waiting for the upload to begin.</p><h3>Step 2: Two connections open</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">PUT /upload/0f033e91-5317-4476-bc89-4a64bc3354d0
  &#8627; sends file as multipart/form-data

GET /upload/progress/0f033e91-5317-4476-bc89-4a64bc3354d0
  &#8627; keeps connection open, receives progress events</code></pre></div><p>Curl commands:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash"># Upload file
curl -i -X PUT http://localhost:8080/upload/0f033e91-5317-4476-bc89-4a64bc3354d0 \
-F file=@./move-crew-details.csv

# Watch progress (in a separate terminal)
curl -i -N http://localhost:8080/upload/progress/0f033e91-5317-4476-bc89-4a64bc3354d0</code></pre></div><p>Two separate HTTP connections. HTTP/1.1 is half-duplex, a single connection can&#8217;t receive a large PUT body and send SSE events simultaneously. One connection uploads, the other streams progress.</p><h3>Step 3: The complete HTTP PUT request</h3><p>Here&#8217;s what actually arrives at the server:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">PUT /upload/0f033e91-5317-4476-bc89-4a64bc3354d0 HTTP/1.1
Host: localhost:8080
Content-Length: 838
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="move-crew-details.csv"
Content-Type: text/csv

(file data bytes)
------WebKitFormBoundary7MA4YWxkTrZu0gW--</code></pre></div><p>The <code>Content-Type</code> header declares <code>multipart/form-data</code> and includes a <code>boundary</code> parameter. That boundary string marks where each part begins and ends in the body. The spec doesn&#8217;t require <code>------</code> prefixes, that&#8217;s just curl&#8217;s convention.</p><h3>Step 4: Validate Content-Type</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">ctype := r.Header.Get("content-type")
mtype, params, err := mime.ParseMediaType(ctype)
if err != nil {
    return apis.NewErrorf(http.StatusBadRequest, "invalid content type. expected multipart/form-data", "content-type sent in request: %s", ctype)
}

if mtype != "multipart/form-data" {
    return apis.NewError(http.StatusBadRequest, "invalid content type. expected multipart/form-data")
}

_, ok := params["boundary"]
if !ok {
    return apis.NewErrorf(http.StatusBadRequest, "boundary not found in multipart/form-data content type", "content-type sent in request: %s", ctype)
}
</code></pre></div><p>Validation happens in three steps:</p><ol><li><p>Parse the header with <code>mime.ParseMediaType</code></p></li><li><p>Verify media type is <code>multipart/form-data</code></p></li><li><p>Verify <code>boundary</code> parameter exists</p></li></ol><p>The server doesn&#8217;t validate the boundary value itself. It just passes it to the multipart reader.</p><h3>Step 5: Stream to disk</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">reader, err := r.MultipartReader()
if err != nil {
    u.log.Error(ctx, "failed to get multipart reader", "action", "r.MultipartReader", "err", err)
    return apis.NewError(http.StatusInternalServerError, "something went wrong. try again.")
}

for {
    part, err := reader.NextPart()
    if err == io.EOF {
        break
    }
    if err != nil {
        u.log.Error(ctx, "error reading next part", "action", "reader.NextPart", "err", err)
        return apis.NewError(http.StatusInternalServerError, "failed to read part")
    }
    
    // extract filename from Content-Disposition header
    cdtype := part.Header.Get("content-disposition")
    cdmtype, cdparams, err := mime.ParseMediaType(cdtype)
    
    filename, ok := cdparams["filename"]
    if ok {
        basepath := filepath.Base(filename)
        tmp, err := os.Create(fmt.Sprintf("./%s", basepath))
        if err != nil {
            return apis.NewError(http.StatusBadRequest, "something went wrong")
        }
        defer tmp.Close()
        
        wr := uploadwriter.New(tmp, uploadid, u.ups)
        n, err := io.Copy(wr, part)
        
        u.ups.SetProgress(uploadid, uploadprogress.Progress{
            Err:        err,
            Total:      uint64(n),
            SoFar:      uint64(n),
            IsComplete: true,
        })
    }
}</code></pre></div><p><code>r.MultipartReader()</code> returns a streaming reader. <code>reader.NextPart()</code> gets each part one at a time. <code>io.Copy</code> allocates a 32KB buffer once, reuses it in a loop.</p><h3>Step 6: Track progress via wrapped writer</h3><p>The <code>uploadwriter</code> wraps the file handle, intercepts every <code>Write</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type Writer struct {
    w     io.Writer
    id    string
    sofar uint64
    ups   *uploadprogress.Store
}

func (uw *Writer) Write(p []byte) (int, error) {
    n, err := uw.w.Write(p)
    uw.sofar += uint64(n)
    uw.ups.SetProgress(uw.id, uploadprogress.Progress{
        Err:   err,
        SoFar: uw.sofar,
    })
    return n, err
}
</code></pre></div><p>Every chunk written to disk updates the progress store. The SSE handler polls this store every 200ms, streams events to the client.</p><h3>Step 7: SSE streams back</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func (u *UploadAPI) UploadProgress(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
    w.Header().Set("content-type", "text/event-stream")
    w.Header().Set("cache-control", "no-cache")
    w.Header().Set("connection", "keep-alive")
    
    id := r.PathValue("uploadid")
    
    for {
        select {
        case &lt;-r.Context().Done():
            return nil
        default:
            progress, err := u.ups.GetProgressById(id)
            if err != nil {
                return apis.NewError(http.StatusNotFound, "upload-id not found")
            }
            
            if progress.IsComplete {
                fmt.Fprintf(w, "event: upload-progress\ndata: {\"progress\": \"%v\", \"complete\": %v}\n\n", progress.SoFar, progress.IsComplete)
                w.(http.Flusher).Flush()
                u.ups.DeleteProgressById(id)
                return nil
            }
            
            if progress.SoFar == 0 &amp;&amp; !progress.IsComplete {
                time.Sleep(2000 * time.Millisecond)
                continue
            }
            
            if progress.Err != nil &amp;&amp; !progress.IsComplete {
                fmt.Fprintf(w, "event: upload-progress\ndata: {\"error\": \"%v\"}\n\n", "failed to upload. Try again")
                w.(http.Flusher).Flush()
                return nil
            }
            
            fmt.Fprintf(w, "event: upload-progress\ndata: {\"progress\": \"%v\", \"complete\": %v}\n\n", progress.SoFar, progress.IsComplete)
            w.(http.Flusher).Flush()
            
            time.Sleep(200 * time.Millisecond)
        }
    }
}
</code></pre></div><p><code>Flush()</code> is critical. Forces each event to be sent as its own HTTP chunk rather than accumulating in the response buffer. Without it, the client sees nothing until the upload completes.</p><p>SSE wire format:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked

event: upload-progress
data: {"progress": "180", "complete": false}

event: upload-progress
data: {"progress": "380", "complete": false}

event: upload-progress
data: {"progress": "838", "complete": true}
</code></pre></div><p>The blank line between events is the SSE delimiter. <code>Transfer-Encoding: chunked</code> gets applied automatically by <code>net/http</code> when writing without a known <code>Content-Length</code>.</p><p>Here&#8217;s what the client actually sees:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">curl -i -N http://localhost:8080/upload/progress/0f033e91-5317-4476-bc89-4a64bc3354d0
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
Date: Sat, 18 Apr 2026 08:10:00 GMT
Transfer-Encoding: chunked

event: upload-progress
data: {"progress": "60", "complete": false}

event: upload-progress
data: {"progress": "80", "complete": false}

.....

event: upload-progress
data: {"progress": "820", "complete": false}

event: upload-progress
data: {"progress": "838", "complete": true}</code></pre></div><div><hr></div><h2>Controlling the Flow: Token Bucket Rate Limiting</h2><p>Now, what if we want to control how fast data flows through this pipeline?</p><p>For a small test file, <code>io.Copy</code> is fast. The entire upload completes in under a millisecond, no SSE events, instant completion. To observe the SSE progress stream meaningfully, the upload needs to be slow enough to emit multiple events.</p><p>That&#8217;s where the token bucket comes in.</p><h3>Where the Token Bucket Sits</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DcRd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DcRd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 424w, https://substackcdn.com/image/fetch/$s_!DcRd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 848w, https://substackcdn.com/image/fetch/$s_!DcRd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 1272w, https://substackcdn.com/image/fetch/$s_!DcRd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DcRd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png" width="1456" height="1218" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1218,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1815482,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/194604916?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DcRd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 424w, https://substackcdn.com/image/fetch/$s_!DcRd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 848w, https://substackcdn.com/image/fetch/$s_!DcRd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 1272w, https://substackcdn.com/image/fetch/$s_!DcRd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe6ca3de-354a-4610-92a0-8a9b2ef6c6a3_1839x1539.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The token bucket sits between the TCP socket buffer and the <code>io.Copy</code> buffer. It&#8217;s an <code>io.Reader</code> wrapper, it wraps the multipart part and controls how fast bytes get pulled from the source.</p><p>Here&#8217;s how it gets wired in:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">wr := uploadwriter.New(tmp, uploadid, u.ups)
n, err := io.Copy(wr, slowreader.New(part))</code></pre></div><p>Without <code>slowreader.New(part)</code>, the code would be:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">n, err := io.Copy(wr, part)</code></pre></div><p>That would be full speed. By wrapping <code>part</code> in <code>slowreader.New(part)</code>, every read from <code>part</code> now goes through the token bucket first.</p><p>Let&#8217;s zoom in on how the bucket actually works.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Yddc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Yddc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 424w, https://substackcdn.com/image/fetch/$s_!Yddc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 848w, https://substackcdn.com/image/fetch/$s_!Yddc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 1272w, https://substackcdn.com/image/fetch/$s_!Yddc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Yddc!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png" width="1724" height="872.6565934065934" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:737,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1724,&quot;bytes&quot;:2210092,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/194604916?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Yddc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 424w, https://substackcdn.com/image/fetch/$s_!Yddc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 848w, https://substackcdn.com/image/fetch/$s_!Yddc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 1272w, https://substackcdn.com/image/fetch/$s_!Yddc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5a92ce3-13cf-473f-abb2-7bf6e0b6789b_3457x1749.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3>The Token Bucket Model</h3><p>Think of it like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">Tokens = currency
Read(p) = purchase
refillRate = how fast you earn currency (100 bytes/sec)
maxBurst = maximum currency you can hold at once (20 bytes)</code></pre></div><p>The bucket starts with some tokens. Every time <code>io.Copy</code> calls <code>Read(p)</code>, the bucket checks: do I have enough tokens to fulfill this read? If yes, read and deduct tokens. If no, wait until enough tokens accumulate.</p><p>Tokens refill passively over time at <code>refillRate</code> bytes per second, up to a maximum of <code>maxBurst</code>.</p><p>Here&#8217;s the actual implementation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">type slowReader struct {
    r              io.Reader
    tokenBal       int64
    refillRate     int64
    lastConsumedAt time.Time
    maxBurst       int64
}

func New(r io.Reader) *slowReader {
    return &amp;slowReader{
        r:              r,
        tokenBal:       1 * 20,  // 20 bytes
        refillRate:     100,     // 100 bytes/sec
        maxBurst:       1 * 20,  // 20 bytes max
        lastConsumedAt: time.Now(),
    }
}</code></pre></div><p>Initial state:</p><ul><li><p><code>tokenBal</code>: 20 bytes (start with a full bucket)</p></li><li><p><code>refillRate</code>: 100 bytes/sec</p></li><li><p><code>maxBurst</code>: 20 bytes (can&#8217;t hold more than this)</p></li><li><p><code>lastConsumedAt</code>: timestamp of last read</p></li></ul><h3>The Read Implementation</h3><p>Every time <code>io.Copy</code> calls <code>Read(p)</code>, this is what happens:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func (s *slowReader) Read(p []byte) (int, error) {
    // Step 1: Calculate how many tokens we've accumulated since last read
    elapsed := time.Since(s.lastConsumedAt).Seconds()
    newTokens := elapsed * float64(s.refillRate)
    bal := min(s.tokenBal+int64(newTokens), s.maxBurst)
    
    // Step 2: Decide what to do based on current balance
    // (three cases below)
}</code></pre></div><p><strong>Step 1</strong> calculates token refill. If 0.5 seconds passed since the last read, and <code>refillRate</code> is 100 bytes/sec, then <code>newTokens = 0.5 &#215; 100 = 50 bytes</code>. But <code>bal</code> is capped at <code>maxBurst = 20</code>, so <code>bal = min(20 + 50, 20) = 20</code>.</p><p><strong>Step 2</strong> splits into three cases based on how many tokens are available.</p><h3>Case 1: Enough tokens for the full read</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">if bal &gt;= int64(len(p)) {
    n, err := s.r.Read(p)
    if err != nil {
        return n, err
    }
    s.tokenBal -= int64(n)
    s.lastConsumedAt = time.Now()
    return n, err
}</code></pre></div><p><code>io.Copy</code> passes a 32KB buffer as <code>p</code>. If the bucket has 20 tokens and <code>len(p) = 32768</code>, this condition is false. But if <code>len(p)</code> were smaller (say, 10 bytes) and <code>bal = 20</code>, this would trigger.</p><p>Read the full buffer, deduct <code>n</code> tokens (actual bytes read), update timestamp, return.</p><h3>Case 2: Some tokens, but less than <code>len(p)</code></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">if bal &gt; 0 &amp;&amp; bal &lt; int64(len(p)) {
    temp := make([]byte, bal)
    n, err := s.r.Read(temp)
    if err != nil {
        return n, err
    }
    copy(p, temp[:n])
    s.tokenBal -= int64(n)
    s.lastConsumedAt = time.Now()
    return n, err
}</code></pre></div><p>This is the common case. We have 20 tokens, but <code>io.Copy</code> is asking for 32KB. We can&#8217;t give 32KB, so we give what we have.</p><p>Steps:</p><ol><li><p>Create a temp buffer sized to <code>bal</code> (20 bytes)</p></li><li><p>Read into that temp buffer (read at most 20 bytes from the source)</p></li><li><p>Copy those bytes into the caller&#8217;s buffer <code>p</code></p></li><li><p>Deduct <code>n</code> tokens</p></li><li><p>Return <code>n</code> (could be 20, could be less if the source gave us less)</p></li></ol><p>The caller (<code>io.Copy</code>) asked for 32KB but got 20 bytes. That&#8217;s fine. The <code>io.Reader</code> contract says: return however many bytes are available, up to <code>len(p)</code>. The caller handles partial reads.</p><h3>Case 3: No tokens, wait</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">// here bal == 0
remaining := min(int64(len(p)), s.maxBurst)

waitTime := (float64(remaining)) / float64(s.refillRate)
time.Sleep(time.Duration(waitTime * float64(time.Second)))

temp := make([]byte, remaining)
n, err := s.r.Read(temp)
copy(p, temp[:n])

s.tokenBal = remaining - int64(n)
s.lastConsumedAt = time.Now()

return n, err</code></pre></div><p>No tokens available. We need to wait for tokens to accumulate.</p><p><strong>Critical line:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">remaining := min(int64(len(p)), s.maxBurst)</code></pre></div><p>The caller asked for <code>len(p) = 32768</code> bytes. But the bucket can only ever hold <code>maxBurst = 20</code> bytes. Waiting for 32KB worth of tokens would take <code>32768 / 100 = 327 seconds</code>.</p><p>That&#8217;s the bug that happened. The upload hung for 327 seconds per read.</p><p>The fix: wait only for <code>min(len(p), maxBurst) = min(32768, 20) = 20</code> tokens.</p><p><strong>Wait time calculation:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">waitTime := (float64(remaining)) / float64(s.refillRate)</code></pre></div><p><code>remaining = 20</code>, <code>refillRate = 100</code>, so <code>waitTime = 20 / 100 = 0.2 seconds</code>.</p><p>Sleep for 0.2 seconds, then tokens have accumulated. Read up to <code>remaining</code> bytes (20 bytes), copy into caller&#8217;s buffer, update state.</p><p>After the sleep, the bucket doesn&#8217;t necessarily have exactly <code>remaining</code> tokens (some might have accumulated before the sleep, some during). The line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">s.tokenBal = remaining - int64(n)</code></pre></div><p>assumes we used <code>n</code> tokens out of the <code>remaining</code> we waited for. If <code>n &lt; remaining</code>, we have leftover tokens for the next read.</p><h3>Why Three Cases?</h3><p>The three-case structure handles all possible states:</p><ol><li><p><strong>Bucket is full or has enough</strong> &#8594; read immediately</p></li><li><p><strong>Bucket has some tokens</strong> &#8594; read what we can, don&#8217;t wait</p></li><li><p><strong>Bucket is empty</strong> &#8594; wait for refill, then read</p></li></ol><p>Without case 2, every partial-token situation would fall through to case 3 and sleep unnecessarily. Case 2 says: &#8220;I have 5 tokens right now, you asked for 32KB, here&#8217;s 5 bytes, come back for more.&#8221;</p><h3>Why Float Division Matters</h3><p>Original broken code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">waitTime := remaining / s.refillRate  // both int64</code></pre></div><p><code>20 / 100 = 0</code> in integer division. Zero wait, zero throttling.</p><p>Fixed code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">waitTime := float64(remaining) / float64(s.refillRate)</code></pre></div><p><code>20.0 / 100.0 = 0.2</code>. Sleep for 0.2 seconds.</p><p>A single type mismatch made throttling disappear silently. No error, no panic, just wrong behavior.</p><h3>Watching It Work</h3><p>With <code>maxBurst = 20</code>, <code>refillRate = 100</code>, uploading an 838-byte file, here&#8217;s what happens:</p><p><strong>Read 1:</strong></p><ul><li><p><code>io.Copy</code> asks for 32KB</p></li><li><p>Bucket has 20 tokens</p></li><li><p>Case 2 triggers: read 20 bytes, return 20 bytes</p></li><li><p>Tokens left: 0</p></li></ul><p><strong>Wait ~0.2 seconds</strong> (next <code>io.Copy</code> iteration)</p><p><strong>Read 2:</strong></p><ul><li><p><code>io.Copy</code> asks for 32KB</p></li><li><p>Bucket has ~20 tokens (refilled during the wait)</p></li><li><p>Case 2 triggers: read 20 bytes, return 20 bytes</p></li><li><p>Tokens left: 0</p></li></ul><p>This repeats. Each read yields ~20 bytes. Total reads: <code>838 / 20 &#8776; 42</code>. Each read waits ~0.2 seconds.</p><p>Expected time: <code>42 &#215; 0.2 = 8.4 seconds</code>.</p><p>SSE output:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">event: upload-progress
data: {"progress": "60", "complete": false}

event: upload-progress
data: {"progress": "180", "complete": false}

event: upload-progress
data: {"progress": "380", "complete": false}

...

event: upload-progress
data: {"progress": "838", "complete": true}</code></pre></div><p>Server logs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">go run cmd/prototypes/main.go
{
  "time": "2026-04-18T13:39:36.837574+05:30",
  "level": "INFO",
  "file": "internal/server/server.go:26",
  "msg": "starting server",
  "port": "0.0.0.0:8080"
}
{
  "time": "2026-04-18T13:39:44.493091+05:30",
  "level": "INFO",
  "file": "internal/middlewares/tracing/tracing.go:26",
  "msg": "Request started",
  "trace": {
    "trace_id": "6be9b4e5-30e6-4cc5-aa30-ee0693aba156",
    "request_method": "POST",
    "request_endpoint": "/upload/initiate"
  }
}
{
  "time": "2026-04-18T13:39:44.493278+05:30",
  "level": "INFO",
  "file": "internal/middlewares/tracing/tracing.go:32",
  "msg": "Request ended. request took",
  "duration": "54.159&#181;s",
  "status": 200,
  "trace": {
    "trace_id": "6be9b4e5-30e6-4cc5-aa30-ee0693aba156",
    "request_method": "POST",
    "request_endpoint": "/upload/initiate"
  }
}
{
  "time": "2026-04-18T13:39:58.593116+05:30",
  "level": "INFO",
  "file": "internal/middlewares/tracing/tracing.go:26",
  "msg": "Request started",
  "trace": {
    "trace_id": "50580d5f-469e-4f40-aa81-f11471abdb3c",
    "request_method": "GET",
    "request_endpoint": "/upload/progress/0f033e91-5317-4476-bc89-4a64bc3354d0"
  }
}
{
  "time": "2026-04-18T13:40:00.146181+05:30",
  "level": "INFO",
  "file": "internal/middlewares/tracing/tracing.go:26",
  "msg": "Request started",
  "trace": {
    "trace_id": "2891a706-c295-408c-bd7c-142745735f04",
    "request_method": "PUT",
    "request_endpoint": "/upload/0f033e91-5317-4476-bc89-4a64bc3354d0"
  }
}
{
  "time": "2026-04-18T13:40:08.385989+05:30",
  "level": "INFO",
  "file": "internal/middlewares/tracing/tracing.go:32",
  "msg": "Request ended. request took",
  "duration": "8.239561818s",
  "status": 200,
  "trace": {
    "trace_id": "2891a706-c295-408c-bd7c-142745735f04",
    "request_method": "PUT",
    "request_endpoint": "/upload/0f033e91-5317-4476-bc89-4a64bc3354d0"
  }
}
{
  "time": "2026-04-18T13:40:08.426012+05:30",
  "level": "INFO",
  "file": "internal/middlewares/tracing/tracing.go:32",
  "msg": "Request ended. request took",
  "duration": "9.832609934s",
  "status": 200,
  "trace": {
    "trace_id": "50580d5f-469e-4f40-aa81-f11471abdb3c",
    "request_method": "GET",
    "request_endpoint": "/upload/progress/0f033e91-5317-4476-bc89-4a64bc3354d0"
  }
}</code></pre></div><p>The PUT took <strong>8.239 seconds</strong>. Expected: ~8.4 seconds.</p><p>The SSE stream stayed open for <strong>9.832 seconds</strong>, slightly longer than the upload because it waited for the final completion event before closing.</p><p>The SSE handler polls every 200ms. Each poll sees progress incrementing by ~20&#8211;40 bytes (depending on timing). The progress climbs steadily until completion.</p><div><hr></div><h2>What This Shows</h2><p>HTTP headers are declarative. The client declares content type in the <code>Content-Type</code> header. Don&#8217;t sniff the body.</p><p>Streaming means never accumulating. <code>r.MultipartReader()</code> + <code>io.Copy</code> keeps RAM at ~160KB for any file size.</p><p>Go&#8217;s <code>io.Reader</code> contract is precise. Caller allocates, reader fills, only <code>p[:n]</code> is valid.</p><p>SSE is HTTP with flushing. <code>Content-Type: text/event-stream</code> + <code>Transfer-Encoding: chunked</code> + <code>Flush()</code>. No magic.</p><p>Rate limiting is three cases, not one. Token bucket <code>Read</code> splits into: enough tokens, some tokens, no tokens.</p><p>Precision matters. Integer division <code>20/100 = 0</code>. Float division <code>20.0/100.0 = 0.2</code>. One type mismatch kills throttling silently.</p><div><hr></div><h2>What&#8217;s Next</h2><p>Prototype 02: HTTP Client Connection Pool Analyzer.</p><p>The goal is to visualize connection reuse, test pool configurations, understand how <code>http.Client</code> actually manages connections under the hood.</p><p>Full code: <a href="https://github.com/vishal2098govind/http-deep-dive">https://github.com/vishal2098govind/http-deep-dive</a></p>]]></content:encoded></item><item><title><![CDATA[Building Trust on the Internet — Part 3: SSH's Two-Keypair Authentication]]></title><description><![CDATA[I've SSHed into EC2 instances dozens of times. But I never understood what that fingerprint prompt was actually checking, or why it mattered.]]></description><link>https://logs.craftedbyvishal.dev/p/building-trust-on-the-internet-part-5d5</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/building-trust-on-the-internet-part-5d5</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Sat, 11 Apr 2026 03:30:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vekO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff41570f-a989-4a97-ab1b-382b7bbe7ccf_1744x2394.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve SSHed into EC2 instances dozens of times. The ritual is familiar &#8212; generate a key-pair in AWS, download the <code>.pem</code> file, <code>chmod 400</code>, then:</p><pre><code><code>ssh -i geek-ec2-kp.pem ec2-user@54.226.167.59</code></code></pre><p>It works. I get a shell. I move on.</p><p>But with this &#8220;Building Trust on the Internet&#8221; series sitting in the back of my mind, I started paying attention to something I&#8217;d been ignoring.</p><p>The first time I connect to a new instance, the terminal pauses:</p><pre><code><code>The authenticity of host '54.226.167.59 (54.226.167.59)' can't be established.
ED25519 key fingerprint is SHA256:WPHp1jPiPTmWCriZegalziBOTc3W0464HIYAdj0XxUU.
Are you sure you want to continue connecting (yes/no)?</code></code></pre><p>I&#8217;d always typed <code>yes</code> without thinking. It&#8217;s part of the ritual. But this time I stopped.</p><p>I already had a key. That&#8217;s what the <code>-i geek-ec2-kp.pem</code> was for. So what was this fingerprint? What was SSH asking me to trust, and why did it matter?</p><p><a href="https://vishalageek.substack.com/p/building-trust-on-the-internet-part">Part 1</a> was about TLS &#8212; how browsers trust servers through certificate chains and PKI. <a href="https://vishalageek.substack.com/p/building-trust-on-the-internet-part-23e">Part 2</a> was about IAM &#8212; how AWS services authenticate without passwords. Both built trust through infrastructure.</p><p>SSH does something different.</p><div><hr></div><h2>1. Two Separate Keypairs</h2><p>It took me a while to untangle this, but the answer is: SSH uses two separate keypairs in every connection.</p><p>I drew this out to make sure I had it straight:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hsWw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hsWw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 424w, https://substackcdn.com/image/fetch/$s_!hsWw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 848w, https://substackcdn.com/image/fetch/$s_!hsWw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 1272w, https://substackcdn.com/image/fetch/$s_!hsWw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hsWw!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png" width="1200" height="566.2087912087912" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:687,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:276264,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/193783060?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hsWw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 424w, https://substackcdn.com/image/fetch/$s_!hsWw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 848w, https://substackcdn.com/image/fetch/$s_!hsWw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 1272w, https://substackcdn.com/image/fetch/$s_!hsWw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e6cd263-dbbe-4f75-a986-efa77eae66e1_1845x871.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><strong>The server host keypair (red circle, left side):</strong></p><p>Generated by the remote machine itself when <code>sshd</code> is first installed or the instance boots for the first time. The private key stays in <code>/etc/ssh/ssh_host_ed25519_key</code> and never leaves the server. The public key gets sent to clients during the handshake, and clients store it in their <code>~/.ssh/known_hosts</code> file.</p><p>This keypair proves &#8220;I am this specific EC2 instance, not a man-in-the-middle.&#8221;</p><p><strong>The user authentication keypair (blue circle, right side):</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads cat geek-ec2-kp.pem
-----BEGIN RSA PRIVATE KEY-----
&lt;base64-encoded private key&gt;
-----END RSA PRIVATE KEY-----%</code></pre></div><p>This is the one I&#8217;m more familiar with. When I created the EC2 key-pair in AWS, AWS generated both keys. I downloaded the private key (the <code>.pem</code> file) and AWS injected the public key into the instance at <code>/home/ec2-user/.ssh/authorized_keys</code> during launch.</p><p>The diagram shows this flow: I keep the private key on my laptop, the instance gets the public key way before the SSH handshake even starts &#8212; either manually via <code>authorized_keys</code> or automatically via EC2&#8217;s cloud-init during instance launch.</p><p>This keypair proves &#8220;I am authorized to log in as <code>ec2-user</code>.&#8221;</p><p><strong>The critical insight from the diagram:</strong></p><p>These two flows are completely independent. The arrow on the left (server host keypair&#8217;s public key) goes from the server to my machine during the handshake. The arrow on the right (user auth keypair&#8217;s public key) goes from my machine to the server <em>before</em> any SSH connection even happens.</p><p>The <code>.pem</code> file I downloaded from AWS? That&#8217;s the user authentication private key. The fingerprint SSH was showing me during first connection? That&#8217;s the hash of the server&#8217;s host public key.</p><p>Two different keys, two different purposes, two different moments in time.</p><div><hr></div><h2>2. The Handshake Flow</h2><p>The next question was: when does each keypair actually get used?</p><p>I traced through the byte-level handshake and split it into three phases:</p><h3>Phase 1 - Setup, Banner Exchange, Algorithm Negotiations</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8OO9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8OO9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 424w, https://substackcdn.com/image/fetch/$s_!8OO9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 848w, https://substackcdn.com/image/fetch/$s_!8OO9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 1272w, https://substackcdn.com/image/fetch/$s_!8OO9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8OO9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png" width="1456" height="1999" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1999,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:451182,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/193783060?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8OO9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 424w, https://substackcdn.com/image/fetch/$s_!8OO9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 848w, https://substackcdn.com/image/fetch/$s_!8OO9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 1272w, https://substackcdn.com/image/fetch/$s_!8OO9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56c94775-c313-4485-880f-df9a5564f2b9_1744x2394.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram shows the AWS setup at the top &#8212; when I created the EC2 key-pair, AWS sent the public key to the instance during launch. That public key sits in <code>/home/ec2-user/.ssh/authorized_keys</code>. I downloaded the private key (the <code>.pem</code> file) and saved it locally.</p><p>When the instance launched, it installed <code>sshd</code> and generated its own server-host-keypair. That private key stays in <code>/etc/ssh/ssh_host_*</code> on the instance.</p><p>Now I run: <code>ssh -i geek-ec2-kp.pem ec2-user@54.226.167.59</code></p><h2>Handshake Begins</h2><h3>Banner exchange (SSH protocol version exchange)</h3><pre><code><code>Server sends "SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5\r\n"
Client sends "SSH-2.0-OpenSSH_9.3\r\n"</code></code></pre><p>Both sides announce their SSH version. Plain text, unencrypted. This is just protocol negotiation &#8212; &#8220;I speak SSH-2.0, here&#8217;s my software version.&#8221;</p><h3>Encryption Algorithm negotiations</h3><p>Both server and client decide on encryption algorithms to be used going further. This includes:</p><ul><li><p>Key exchange method (like <code>curve25519-sha256</code> or <code>ecdh-sha2-nistp256</code>)</p></li><li><p>Encryption cipher (like <code>aes128-gcm</code>)</p></li><li><p>MAC algorithm</p></li><li><p>Compression</p></li></ul><p>Still unencrypted at this point. They&#8217;re just agreeing on what tools they&#8217;ll use once the secure channel is established.</p><h3>Phase 2 - Key Exchange, Server Authentication, Client Requests User Auth</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kgrs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kgrs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 424w, https://substackcdn.com/image/fetch/$s_!kgrs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 848w, https://substackcdn.com/image/fetch/$s_!kgrs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 1272w, https://substackcdn.com/image/fetch/$s_!kgrs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kgrs!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png" width="1200" height="1217.3076923076924" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1477,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:859084,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/193783060?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kgrs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 424w, https://substackcdn.com/image/fetch/$s_!kgrs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 848w, https://substackcdn.com/image/fetch/$s_!kgrs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 1272w, https://substackcdn.com/image/fetch/$s_!kgrs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5be200fb-5a3d-4c6a-8fcf-3d3e98adab6b_2742x2781.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Key exchange</h3><p>This is where the server&#8217;s host keypair comes in.</p><p><strong>Client sends its ephemeral public key</strong></p><p>Just for this session, used for deriving the shared secret and symmetric key. The client generated an ephemeral keypair (only lives for this one connection).</p><p><strong>Server sends:</strong></p><p>This is where I got confused initially &#8212; the server sends <strong>two</strong> public keys:</p><ol><li><p><strong>Server-host-keypair&#8217;s public key</strong> along with a signed hash to authenticate itself (for client to verify server)</p></li><li><p><strong>Server&#8217;s ephemeral public key</strong> for key exchange</p></li><li><p><strong>Signature of a hash</strong> (on strings which client also can derive on its own end) signed using the server-host-keypair&#8217;s private key</p></li></ol><p>I had to sit with this to understand why two keys. One is for server authentication, the other is for the actual key exchange so both sides can derive a shared secret. The ephemeral one is short-lived, only exists during this handshake. The host key is long-lived &#8212; it&#8217;s the same key that gets sent every time from this server during every handshake, which is why the client adds it to <code>known_hosts</code>.</p><p>Both sides can now compute the same shared secret via ECDH. But the signature is the critical part. It proves &#8220;I own the private key matching this host public key, and I&#8217;m binding my identity to this specific key exchange.&#8221;</p><p>The hash being signed includes the banners, the algorithm lists, both ephemeral keys, and the shared secret from ECDH. The server signs it, the client verifies it.</p><h3>Server authentication by client</h3><p>The client takes the server&#8217;s host public key and checks <code>~/.ssh/known_hosts</code>.</p><p>The client verifies the server by preparing the same hash and comparing it with the server&#8217;s hash (obtained by decrypting the signature using the server-host-keypair&#8217;s public key which the server sent during key exchange).</p><p>It also adds this public key (base64-encoded) to <code>known_hosts</code> against this particular host-name that was used during <code>ssh -i /path/to/pem ec2-user@&lt;host-name&gt;</code>.</p><p>The <code>&lt;host-name&gt;</code> can be a public IP address of EC2 instance or domain name if given.</p><p>If it&#8217;s a first connection, the client shows me the fingerprint and asks if I trust it. If it&#8217;s a repeat connection, the client verifies the key matches what&#8217;s stored. If there&#8217;s a mismatch, connection refused &#8212; that&#8217;s the MITM warning.</p><h3>From here on, the channel is encrypted</h3><p>As the diagram shows, the shared session keys (symmetric keys) are established. All further communication uses these symmetric keys for encryption.</p><h3>Client initiates/requests user authentication</h3><p>Client sends <code>SSH_MSG_SERVICE_REQUEST</code> and server sends <code>SSH_MSG_SERVICE_ACCEPT</code>.</p><p>This is because SSH can be used for other purposes as well like port-forwarding, and not only remote-login. Since here we are using SSH to authenticate to an AWS EC2 Linux instance, it means that I want to authenticate myself by sending <code>SSH_MSG_SERVICE_REQUEST</code> and server says &#8220;ok go ahead and authenticate yourself&#8221; by sending <code>SSH_MSG_SERVICE_ACCEPT</code>.</p><h3>Phase 3 - User Authentication (Probe + Signature), Handshake Done</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!g5LY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!g5LY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 424w, https://substackcdn.com/image/fetch/$s_!g5LY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 848w, https://substackcdn.com/image/fetch/$s_!g5LY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 1272w, https://substackcdn.com/image/fetch/$s_!g5LY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!g5LY!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png" width="1200" height="1420.8791208791208" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1724,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:825599,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/193783060?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!g5LY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 424w, https://substackcdn.com/image/fetch/$s_!g5LY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 848w, https://substackcdn.com/image/fetch/$s_!g5LY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 1272w, https://substackcdn.com/image/fetch/$s_!g5LY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F763896f4-998e-4cb0-8fab-e7ca9167a025_2585x3060.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>User authentication by server</h3><p>Now, inside the encrypted tunnel, I finally prove I&#8217;m authorized to log in.</p><p>This happens in two steps:</p><p><strong>Step 1: Probe request (User Authentication - Probe Phase)</strong></p><p>Signing is an expensive process, so first the client will send a probe request (<code>SSH_MSG_USERAUTH_REQUEST (Probe)</code>) without signature, just for the server to verify basic details like if the requested user (say <code>ec2-user</code> or <code>ubuntu</code>) is present or not in the system.</p><p>After the server validates basic details, it sends the client <code>SSH_MSG_USERAUTH_PK_OK</code> saying &#8220;you can now send your signature and I will verify using my public key which I was shared when the ec2-key-pair was attached during my launch (or user shared with me when he did ssh-keygen).&#8221;</p><p>After basic validation check, server responds with <code>SSH_MSG_USERAUTH_PK_OK</code>.</p><p><strong>Step 2: Actual signature (User Authentication by server)</strong></p><p>Now the client prepares a <code>signature_blob</code> which the server can also prepare during its verification, and signs it using the ec2-key-pair&#8217;s private key which was downloaded while creating the key-pair (or created while doing ssh-keygen).</p><p>Client sends <code>SSH_MSG_USERAUTH_REQUEST (with signature)</code>.</p><p>The server then verifies the signature using the ec2-keypair&#8217;s public key which was given to it while launching the ec2 instance, and if verified, authenticates client.</p><p>Server responds with <code>SSH_MSG_USERAUTH_SUCCESS</code>.</p><p>If valid, shell granted.</p><h3>Handshake done</h3><p>The diagram shows the Amazon Linux 2023 prompt &#8212; I&#8217;m now logged into the instance.</p><h3>The key insight from these diagrams</h3><p>Encryption happens before user authentication. The server proves its identity (via host key signature) before I ever prove mine. SSH secures the pipe first, then checks authorization.</p><p>The user keypair is only used once, during login. After that, all communication is encrypted with the <strong>session keys</strong> &#8212; symmetric keys derived from the ephemeral key exchange.</p><p>This mirrors what I covered in <a href="https://vishalageek.substack.com/p/building-trust-on-the-internet-part">Part 1</a>: asymmetric encryption (the host keypair, the user keypair) is used to establish trust and identity, but the actual data transmission uses symmetric encryption (AES-GCM) because it&#8217;s much faster for large amounts of data. The session keys are what handle all the terminal input/output, file transfers, everything after login. Asymmetric crypto did its job during the handshake. From here on, it&#8217;s just fast symmetric encryption.</p><div><hr></div><h2>3. known_hosts Isn&#8217;t &#8220;Trusted Hosts&#8221;</h2><p>When I typed <code>yes</code> at that fingerprint prompt, SSH saved the server&#8217;s host public key to <code>~/.ssh/known_hosts</code>:</p><pre><code><code>54.226.167.59 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDxD6gi+FNijA1n40q0xZy5t8YyPL/CeqX2HTzmLhKnR</code></code></pre><p>This isn&#8217;t a list of &#8220;servers I trust.&#8221; It&#8217;s a list of &#8220;this server has this public key.&#8221;</p><p>The difference matters.</p><p>On the next connection to <code>54.226.167.59</code>, SSH doesn&#8217;t ask me again. It checks: does the public key the server just sent match what&#8217;s stored in <code>known_hosts</code>? If yes, connect silently. If no, scream.</p><blockquote><p>SSH tracks servers by cryptographic identity, not by hostname.</p></blockquote><p>The fingerprint (<code>SHA256:WPHp1jPiPTmWCriZegalziBOTc3W0464HIYAdj0XxUU</code>) is just a hash of that public key, displayed in a human-friendly format. Comparing a 43-character hash is easier than comparing a 256-byte key.</p><div><hr></div><h2>4. The Experiments</h2><p>I wanted to see what actually triggers the warnings. So I spun up EC2 instances and ran through different scenarios.</p><h3>Experiment 1: First connection</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@54.226.167.59
The authenticity of host '54.226.167.59 (54.226.167.59)' can't be established.
ED25519 key fingerprint is SHA256:WPHp1jPiPTmWCriZegalziBOTc3W0464HIYAdj0XxUU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '54.226.167.59' (ED25519) to the list of known hosts.</code></pre></div><p>That fingerprint is the SHA-256 hash of the server&#8217;s host public key. SSH is asking me to verify it. I typed <code>yes</code>. It got saved to <code>~/.ssh/known_hosts</code>.</p><h3>Experiment 2: Reboot</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads echo "I am restarting my ec2 instance now"
I am restarting my ec2 instance now
&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@54.226.167.59
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '-&gt;
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/
[ec2-user@ip-172-31-20-136 ~]$</code></pre></div><p>No prompt. SSH logged me in silently. Same instance, same host keys. The files in <code>/etc/ssh/ssh_host_*</code> survived the reboot.</p><h3>Experiment 3: Stop/Start (new IP, same instance)</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads echo "now i have stopped and restarted"
now i have stopped and restarted
&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@54.196.250.140
The authenticity of host '54.196.250.140 (54.196.250.140)' can't be established.
ED25519 key fingerprint is SHA256:WPHp1jPiPTmWCriZegalziBOTc3W0464HIYAdj0XxUU.
This host key is known by the following other names/addresses:
    ~/.ssh/known_hosts:42: 54.226.167.59
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes</code></pre></div><p>Same fingerprint as before. Different IP. SSH noticed: &#8220;I&#8217;ve seen this key before, on <code>54.226.167.59</code>.&#8221;</p><p>Stop/start preserves the EBS volume. The host keypair files stayed intact. New IP, same cryptographic identity.</p><p>I checked <code>known_hosts</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads cat ~/.ssh/known_hosts | grep 54.196.250.140
54.196.250.140 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDxD6gi+FNijA1n40q0xZy5t8YyPL/CeqX2HTzmLhKnR</code></pre></div><p>That base64 blob is the server&#8217;s host public key. The fingerprint is just <code>SHA256(that_blob)</code> for human readability.</p><h3>Experiment 4: Terminate/Launch (new instance, same user keypair)</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads echo "now lets try to terminate the instance and launch new instance with same geek-ec2-kp keypair"
now lets try to terminate the instance and launch new instance with same geek-ec2-kp keypair
&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@54.242.80.236
The authenticity of host '54.242.80.236 (54.242.80.236)' can't be established.
ED25519 key fingerprint is SHA256:jrRK6H1dssHviNzrP2zm7ucoZOzG2zPjVprL7Bz26Pw.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes</code></pre></div><p>Different fingerprint. New instance, new host keys generated on first boot. I&#8217;m still using the same user keypair (<code>geek-ec2-kp.pem</code>), but the server&#8217;s identity changed.</p><h3>Experiment 5: The MITM trap (same IP, different instance)</h3><p>This is the scenario SSH is built to catch.</p><p>I am trying to reproduce MITM warning. so I am using Elastic IP to do that.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads echo "i am trying to reproduce MITM warning. so I am using Elastic IP to do that."
i am trying to reproduce MITM warning. so I am using Elastic IP to do that.
&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@98.90.74.60
The authenticity of host '98.90.74.60 (98.90.74.60)' can't be established.
ED25519 key fingerprint is SHA256:p8l/VDBRYXPbiHLijdOjdSf8aiRWAblFJH29CWaTDLM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes</code></pre></div><p>Trusted it. Then I terminated that instance and launched a new one with the same Elastic IP.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads echo "now I will terminate this instance and use the same Elastic IP to attach it to another instance"
now I will terminate this instance and use the same Elastic IP to attach it to another instance
&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@98.90.74.60
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:ZeRBDXzEqoTaF4lI2cC1ihBoUST27cXg/AdjQ2cqrUA.
Please contact your system administrator.
Add correct host key in /Users/govind/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/govind/.ssh/known_hosts:48
Host key for 98.90.74.60 has changed and you have requested strict checking.
Host key verification failed.</code></pre></div><p>Same IP. Different fingerprint. SSH refused to connect.</p><p>An attacker could steal my Elastic IP, launch their own server, associate the IP. But they can&#8217;t generate the same host keypair. Different server, different public key. Always.</p><p>To fix it, I removed the old entry:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads ssh-keygen -R 98.90.74.60
# Host 98.90.74.60 found: line 47
# Host 98.90.74.60 found: line 48
/Users/govind/.ssh/known_hosts updated.
Original contents retained as /Users/govind/.ssh/known_hosts.old</code></pre></div><p>Then connected again:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10140;  Downloads ssh -i geek-ec2-kp.pem ec2-user@98.90.74.60
The authenticity of host '98.90.74.60 (98.90.74.60)' can't be established.
ED25519 key fingerprint is SHA256:ZeRBDXzEqoTaF4lI2cC1ihBoUST27cXg/AdjQ2cqrUA.
Are you sure you want to connect (yes/no/[fingerprint])? yes</code></pre></div><p>Fresh trust established with the new instance.</p><div><hr></div><h2>5. TOFU: Trust On First Use</h2><p>On that first connection, SSH asked me to verify the fingerprint. It didn&#8217;t automatically trust the server. There&#8217;s no certificate authority. No pre-installed list of trusted servers. No DigiCert for SSH.</p><p>I am the trust anchor.</p><p>If I verify the fingerprint out-of-band &#8212; check the AWS console system log, call the admin, whatever &#8212; I can type <code>yes</code> safely. The fingerprint in the console matches what SSH showed me, so I know I&#8217;m talking to the real server.</p><p>AWS provides the fingerprint in the system log:</p><pre><code><code>EC2 Console &#8594; Instance &#8594; Actions &#8594; Monitor and troubleshoot &#8594; Get system log</code></code></pre><pre><code><code>-----BEGIN SSH HOST KEY FINGERPRINTS-----
256 SHA256:ZeRBDXzEqoTaF4lI2cC1ihBoUST27cXg/AdjQ2cqrUA root@ip-172-31-22-221 (ED25519)
-----END SSH HOST KEY FINGERPRINTS-----</code></code></pre><p>Match this with what SSH shows you. If they match, type <code>yes</code>. You&#8217;ve verified it out-of-band. No MITM possible.</p><p>If I blindly type <code>yes</code> without checking, I&#8217;m vulnerable to a man-in-the-middle on that first connection. An attacker sitting between me and the real server could show me their host key instead. I&#8217;d save it to <code>known_hosts</code>, and from that point on, all my SSH traffic would go through the attacker.</p><p>But after the first connection, SSH enforces consistency. The host key is locked in. Any change triggers the scary warning.</p><blockquote><p>This is the opposite of TLS.</p></blockquote><p>TLS uses a hierarchy of certificate authorities. My browser ships with ~100 pre-trusted root CAs. DigiCert, Let&#8217;s Encrypt, Sectigo. Any server with a valid certificate from any of these CAs is automatically trusted. I never see a fingerprint prompt when I visit a new website.</p><p>SSH has no global trust infrastructure. You can run your own SSH CA if you want &#8212; generate a CA keypair, sign your servers&#8217; host keys, configure clients to trust your CA&#8217;s public key. But it&#8217;s opt-in. And it&#8217;s rare. Most people use TOFU and manually verify fingerprints for critical servers.</p><p>The trust models reflect the use cases:</p><ul><li><p>TLS: millions of websites, unknown in advance, need automatic trust</p></li><li><p>SSH: dozens of servers, infrastructure you control, can verify manually</p></li></ul><div><hr></div><h2>6. Why AWS Doesn&#8217;t Use SSH Certificates</h2><p>When you launch an EC2 instance, AWS generates the host keypair during first boot. AWS doesn&#8217;t sign it with a CA. The instance just presents a raw public key.</p><p>AWS could run an SSH CA. They could sign every EC2 instance&#8217;s host key with an AWS root CA, distribute that CA&#8217;s public key to every AWS customer, and eliminate TOFU entirely.</p><p>They don&#8217;t.</p><p>Why? Because the trust boundary is different. AWS doesn&#8217;t want to be the global SSH authority. You launched the instance. You control it. You verify it.</p><p>In practice, most people skip the fingerprint verification and rely on context. &#8220;I just launched this instance 30 seconds ago, the timing suggests it&#8217;s real, I&#8217;ll trust it.&#8221; That&#8217;s TOFU. It works most of the time. It&#8217;s not bulletproof.</p><div><hr></div><h2>7. Closing the Loop</h2><p>Three models, three answers to the same question: how do you trust someone you&#8217;ve never met?</p><p><strong>TLS (<a href="https://vishalageek.substack.com/p/building-trust-on-the-internet-part">Part 1</a>):</strong> Trust a hierarchy. DigiCert vouches for the server, my browser trusts DigiCert. Scales to millions of websites.</p><p><strong>AWS IAM (<a href="https://vishalageek.substack.com/p/building-trust-on-the-internet-part-23e">Part 2</a>):</strong> Trust temporary credentials. Roles define boundaries, STS issues tokens, they expire every hour. Scales to ephemeral infrastructure.</p><p><strong>SSH (Part 3):</strong> Trust what you verify yourself. On first connection, you check the fingerprint. After that, SSH watches for swaps. Scales to dozens of known servers.</p><p>None of these is &#8220;better.&#8221; They&#8217;re optimized for different threat models.</p><p>The fingerprint prompt I used to ignore? It&#8217;s SSH asking: &#8220;Have you verified this server&#8217;s identity out-of-band, or are you trusting on first use?&#8221; That&#8217;s the entire security model in one question.</p><p>I know the answer now.</p><div><hr></div><p><em>The next part will probably be JWT and OAuth &#8212; how applications represent identity and delegate access outside of a single platform. Or it might be something else entirely, depending on what breaks next.</em></p>]]></content:encoded></item><item><title><![CDATA[Building Trust on the Internet — Part 2: AWS IAM and AWS STS]]></title><description><![CDATA[TLS told me the server is who it claims to be. Inside AWS, a different question takes over &#8212; who is the caller, and what are they allowed to do?]]></description><link>https://logs.craftedbyvishal.dev/p/building-trust-on-the-internet-part-23e</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/building-trust-on-the-internet-part-23e</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Tue, 07 Apr 2026 22:14:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kmHk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kmHk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kmHk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 424w, https://substackcdn.com/image/fetch/$s_!kmHk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 848w, https://substackcdn.com/image/fetch/$s_!kmHk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 1272w, https://substackcdn.com/image/fetch/$s_!kmHk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kmHk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png" width="831" height="944" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:944,&quot;width&quot;:831,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:151086,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/193515841?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kmHk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 424w, https://substackcdn.com/image/fetch/$s_!kmHk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 848w, https://substackcdn.com/image/fetch/$s_!kmHk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 1272w, https://substackcdn.com/image/fetch/$s_!kmHk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F605794ff-f0a3-4168-bb66-a98dc031a2ac_831x944.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In <a href="https://open.substack.com/pub/vishalageek/p/building-trust-on-the-internet-part?utm_campaign=post-expanded-share&amp;utm_medium=web">Part 1</a>, I traced how HTTPS works from the ground up. Asymmetric cryptography, certificate chains, the TLS handshake. By the end, a browser and a server could talk securely over an untrusted network.</p><p>But that only solves one part of the problem.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://logs.craftedbyvishal.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Engineer&#8217;s Notebook! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><blockquote><p>TLS answers: <em>&#8220;Can I trust this server?&#8221;</em> AWS IAM answers: <em>&#8220;Can I trust this caller, and what are they allowed to do?&#8221;</em></p></blockquote><p>These are different questions. And inside AWS, the second one comes up constantly &#8212; every time a Lambda reads from S3, every time a service talks to another service, every time a developer runs a CLI command.</p><p>This article is about that second question. Not the theory. The actual mechanics.</p><p><em>(JWT and OAuth are coming in a separate part. This article focuses on how AWS itself solves auth at the infrastructure level.)</em></p><div><hr></div><h2>1. The Problem AWS IAM Solves</h2><p>When I first started building on AWS, the instinct was to use access keys. Generate a key, drop it into an environment variable, done. Lambda reads from S3. EC2 writes to DynamoDB. Glue pulls from RDS. Each service authenticates with a static credential.</p><p>That falls apart quickly.</p><ul><li><p>Keys leak through logs, environment dumps, or accident</p></li><li><p>Rotating them across dozens of services is painful</p></li><li><p>There&#8217;s no natural expiry</p></li><li><p>A leaked key stays valid until someone manually revokes it</p></li></ul><p>AWS IAM solves this with a different model entirely.</p><blockquote><p>No passwords. No static keys in services. Every identity is temporary, scoped, and auto-rotating.</p></blockquote><p>The mechanism that makes this possible is <strong>STS &#8212; the Security Token Service.</strong></p><div><hr></div><h2>2. Who Are You in AWS?</h2><p>Before getting into STS, there&#8217;s a foundational concept worth getting right.</p><p>In AWS, every entity that makes a request is called a <strong>Principal</strong> &#8212; the thing that is authenticated and making the request.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Principal Type      Example
---------------------------------------------------------------------------
IAM User            arn:aws:iam::123456789:user/vishal
IAM Role            arn:aws:iam::123456789:role/my-lambda-role
AWS Service         lambda.amazonaws.com
AWS Account         arn:aws:iam::123456789:root
Federated identity  via SAML / OIDC / Cognito
Everyone            *</code></pre></div><p>I used to think of IAM Users as the primary identity in AWS. In practice, <strong>IAM Roles</strong> are the more important primitive. Roles aren&#8217;t tied to a person. They&#8217;re tied to a purpose. Any entity that AWS trusts can assume a role and act with its permissions.</p><p>That word &#8212; <em>assume</em> &#8212; is load-bearing. Everything else flows from it.</p><div><hr></div><h2>3. The Two Policies Every Role Has</h2><p>An IAM Role is defined by two policies. Both matter.</p><p><strong>Trust Policy</strong> &#8212; who is allowed to assume this role:</p><pre><code><code>{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
</code></code></pre><p>This says: only the Lambda service is allowed to assume this role.</p><p><strong>Permission Policy</strong> &#8212; what this role is allowed to do:</p><pre><code><code>{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}
</code></code></pre><p>This says: whoever assumes this role can read and write objects in <code>my-bucket</code>.</p><p>Two separate concerns. Who can become this identity. What this identity can do.</p><blockquote><p>Trust policy = the door. Permission policy = what&#8217;s behind it.</p></blockquote><div><hr></div><h2>4. How a Request Actually Gets to AWS</h2><p>Before following a request end-to-end, there&#8217;s one more piece to understand: how do humans and applications even reach AWS?</p><ul><li><p><strong>AWS Console</strong> &#8212; password + MFA. Log in, get a session.</p></li><li><p><strong>AWS CLI</strong> &#8212; access key + secret, configured once. The CLI signs every request on behalf of the caller.</p></li><li><p><strong>AWS SDK</strong> &#8212; same access key + secret, OR an IAM Role if running on AWS compute. The SDK resolves credentials automatically via a <strong>credential provider chain</strong> &#8212; it checks env vars, then credentials file, then the instance metadata service, and so on. The same code works locally and on EC2 without modification.</p></li><li><p><strong>Direct API</strong> &#8212; everything above is ultimately a wrapper. Under the hood, every AWS API call is an HTTPS request signed with <strong>Signature Version 4 (SigV4)</strong>.</p></li></ul><p>SigV4 is the equivalent of TLS&#8217;s handshake signature &#8212; it proves the caller holds a specific key, and it binds the request to a specific time, region, and service so it can&#8217;t be replayed or redirected.</p><p>I&#8217;ll show exactly what SigV4 looks like later in this article.</p><div><hr></div><h2>5. Following a Request End-to-End</h2><p>The clearest way to understand this is to follow one request through the entire system. A Lambda function reading a file from S3.</p><div><hr></div><h3>Step 1: A role is attached to the Lambda</h3><p>The Lambda is created with an IAM Role attached to it. The trust policy on that role says <code>lambda.amazonaws.com</code> is allowed to assume it. The permission policy says the role can call <code>s3:GetObject</code> on <code>my-bucket</code>.</p><pre><code><code>aws lambda create-function \
  --function-name my-function \
  --role arn:aws:iam::123456789012:role/my-lambda-role \
  ...
</code></code></pre><p>Nothing has happened yet. The role is just a definition.</p><div><hr></div><h3>Step 2: Lambda is invoked, AWS runtime calls STS</h3><p>The function is invoked. Before the handler code even starts, the AWS Lambda runtime calls STS automatically:</p><pre><code><code>POST https://sts.amazonaws.com/

Action=AssumeRole
&amp;RoleArn=arn:aws:iam::123456789012:role/my-lambda-role
&amp;RoleSessionName=my-function-execution-abc123
&amp;DurationSeconds=3600
</code></code></pre><p>This happens invisibly. No code written, no configuration needed. The platform handles it before handing control to the handler.</p><div><hr></div><h3>Step 3: STS returns temporary credentials</h3><p>STS checks the trust policy &#8212; does it allow <code>lambda.amazonaws.com</code> to assume this role? Yes. It returns:</p><pre><code><code>{
  "Credentials": {
    "AccessKeyId":     "ASIAXXXXXXXXXXX12345",
    "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "SessionToken":    "FwoGZXIvYXdzEJr...long-token...Tuw==",
    "Expiration":      "2026-04-08T11:00:00Z"
  }
}
</code></code></pre><p>The <code>AccessKeyId</code> starts with <code>ASIA</code>. That prefix always means temporary STS credentials. Permanent IAM user keys start with <code>AKIA</code>. One letter carries the whole story.</p><p>These credentials are injected into the Lambda execution environment before the handler starts.</p><div><hr></div><h3>Step 4: SDK picks up credentials &#8212; without any explicit configuration</h3><p>The handler code is simply:</p><pre><code><code>cfg, err := config.LoadDefaultConfig(context.TODO())
s3Client := s3.NewFromConfig(cfg)
</code></code></pre><p>No credentials passed explicitly. The SDK walks the credential provider chain:</p><pre><code><code>1. Hard-coded in code?               &#8594; no
2. AWS_ACCESS_KEY_ID env var?        &#8594; no
3. ~/.aws/credentials file?          &#8594; no
4. ECS container endpoint?           &#8594; no
5. Lambda runtime env vars?          &#8594; YES &#10003;
   AWS_ACCESS_KEY_ID     = ASIAXXXXXXXXXXX12345
   AWS_SECRET_ACCESS_KEY = wJalrXUtnFEMI/...
   AWS_SESSION_TOKEN     = FwoGZXIvYXdzEJr...
</code></code></pre><p>The STS call already happened before your code started. The SDK&#8217;s job here is purely credential discovery, not credential generation. It finds the credentials sitting in env vars and picks them up.</p><p>This is why the same code works locally &#8212; locally it picks up from <code>~/.aws/credentials</code>. On Lambda it picks up from env vars. Not a single line of code changes.</p><div><hr></div><h3>Step 5: SDK builds the canonical request</h3><p>Your code calls <code>s3Client.GetObject(...)</code>. The SDK translates this into an HTTP request, but before sending it, it has to sign it. To sign it, it first needs to normalize it.</p><p>The request could be expressed in many equivalent ways &#8212; different header casing, different query string order, trailing slashes. HMAC-SHA256 is sensitive to every character. If the signer and verifier normalize differently, the signatures won&#8217;t match.</p><p>The solution is a <strong>canonical request</strong> &#8212; one strict, agreed-upon representation of the request:</p><pre><code><code>GET
/my-file.txt

host:my-bucket.s3.amazonaws.com
x-amz-content-sha256:e3b0c44298fc1c149afb...
x-amz-date:20260408T100000Z
x-amz-security-token:FwoGZXIvYXdzEJr...

host;x-amz-content-sha256;x-amz-date;x-amz-security-token

e3b0c44298fc1c149afb...
</code></code></pre><p>The word <em>canonical</em> means exactly that &#8212; one authoritative form. Both the SDK (signing) and AWS (verifying) apply the same normalization rules and arrive at the identical string. Then they both sign it. The signatures match, or the request is rejected.</p><p>Normalization rules:</p><ul><li><p>HTTP method &#8594; always uppercase</p></li><li><p>URI path &#8594; always normalized</p></li><li><p>Query string &#8594; sorted alphabetically</p></li><li><p>Header names &#8594; always lowercase, sorted alphabetically</p></li><li><p>Header values &#8594; whitespace trimmed</p></li><li><p>Body &#8594; always SHA256 hashed, never included raw</p></li></ul><div><hr></div><h3>Step 6: SigV4 signing</h3><pre><code><code># Hash the canonical request
SHA256(canonical_request) = "3b4c5d6e..."

# Build string to sign
AWS4-HMAC-SHA256
20260408T100000Z
20260408/us-east-1/s3/aws4_request
3b4c5d6e...

# Derive signing key &#8212; scoped per date, region, service
kDate    = HMAC-SHA256("AWS4" + SecretAccessKey, "20260408")
kRegion  = HMAC-SHA256(kDate,    "us-east-1")
kService = HMAC-SHA256(kRegion,  "s3")
kSigning = HMAC-SHA256(kService, "aws4_request")

# Final signature
Signature = HMAC-SHA256(kSigning, string_to_sign)
</code></code></pre><p>The signing key is derived fresh for every date, region, and service combination. A key valid for S3 today cannot sign a DynamoDB request. A key for <code>us-east-1</code> cannot be used in <code>eu-west-1</code>. The scope is baked into the key itself.</p><p>The signature goes into the <code>Authorization</code> header:</p><pre><code><code>Authorization: AWS4-HMAC-SHA256
  Credential=ASIAXXXXXXXXXXX12345/20260408/us-east-1/s3/aws4_request,
  SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token,
  Signature=fe5f80f77d5fa3beca...
</code></code></pre><p><strong>Wait &#8212; where are the private and public keys?</strong></p><p>Coming from TLS, this was the question I had too. SigV4 doesn&#8217;t use asymmetric cryptography at all. There are no private or public keys here.</p><p>TLS uses asymmetric signing because the verifier (the browser) doesn&#8217;t know the signer (the server) in advance. The public key travels via a certificate, and anyone with the public key can verify the signature without knowing the private key.</p><p>SigV4 uses <strong>HMAC &#8212; a symmetric scheme</strong>. Both sides use the same secret:</p><pre><code><code>TLS   &#8594; Sign(private_key, data)   | Verify(public_key, signature, data)
SigV4 &#8594; HMAC(secret_key, data)    | HMAC(secret_key, data) &#8594; compare
</code></code></pre><p>The shared secret is the <code>SecretAccessKey</code> from the STS credentials. The SDK has it because STS returned it. AWS has it because STS generated it in the first place. When S3 receives the request, it asks STS for the <code>SecretAccessKey</code> associated with this <code>AccessKeyId</code> and <code>SessionToken</code>, then recomputes the signature independently.</p><p>HMAC works here because the trust model is different &#8212; AWS issued the secret itself, so both sides already share it. No certificate infrastructure needed. And since the secret is short-lived (expires in an hour), the risk profile is much lower than a long-lived private key.</p><div><hr></div><h3>Step 7: S3 validates everything</h3><p>S3 receives the request and runs through a checklist:</p><ol><li><p>Extracts <code>AccessKeyId</code> from the <code>Authorization</code> header. <code>ASIA</code> prefix means it also expects a <code>SessionToken</code>.</p></li><li><p>Calls STS internally to validate the <code>SessionToken</code> &#8212; is it valid, non-expired, which role does it belong to?</p></li><li><p>Independently recomputes the canonical request and signature using the same rules. Compares with what arrived in the header.</p></li><li><p>Checks IAM &#8212; does <code>my-lambda-role</code> have <code>s3:GetObject</code> on this specific bucket and key?</p></li><li><p>Any explicit Deny anywhere? Any bucket policy that blocks this?</p></li><li><p>All checks pass. Response returned.</p></li></ol><p>The request signature is verified by recomputing it independently. AWS never trusts the signature &#8212; it re-derives it. This is the same principle as TLS certificate verification &#8212; trust is established by recomputing, not by accepting.</p><div><hr></div><h3>Step 8: Credentials expire, cycle repeats</h3><p>At <code>Expiration</code>, the credentials become useless &#8212; even if an attacker intercepted them. The Lambda runtime calls STS again automatically before expiry. Fresh credentials are injected. The handler code sees none of this cycle.</p><div><hr></div><h2>6. Who Makes the STS Call?</h2><p>The Lambda example showed the platform calling STS before your code runs. But that&#8217;s not universal.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Service                              Who calls STS
------------------------------------------------------------------------------
Lambda, ECS, Fargate, App Runner     AWS platform, before code starts.
                                     Credentials pre-injected into env vars
                                     or container credentials endpoint.

EC2                                  IMDS at 169.254.169.254 calls STS and
                                     caches the result. The SDK calls IMDS,
                                     never STS directly.

EKS (via IRSA)                       A different mechanism &#8212; worth its own
                                     article.

Cross-account, Federation, CLI       Explicit AssumeRole call in application
                                     code.</code></pre></div><p>The mechanism is always the same. STS, temporary credentials, signed requests. What changes is who initiates the STS call.</p><p><strong>EC2 and IMDS &#8212; a closer look</strong></p><p>Lambda is short-lived, so pre-injecting credentials before the handler starts works fine. EC2 is different &#8212; an instance can run for months. Pre-injected credentials would expire with no runtime to refresh them.</p><p>AWS solves this with the <strong>Instance Metadata Service (IMDS)</strong> &#8212; a special HTTP server running on every EC2 instance at a fixed link-local IP:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">http://169.254.169.254</code></pre></div><p>This IP is not routable. Nothing outside the instance can reach it. Only processes running on that specific EC2 instance can hit it.</p><p>IMDS exposes the instance&#8217;s IAM credentials at a well-known path:</p><pre><code><code>curl http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-role
</code></code></pre><pre><code><code>{
  "AccessKeyId":     "ASIAXXXXXXXXXXX12345",
  "SecretAccessKey": "wJalrXUtnFEMI/...",
  "Token":           "FwoGZXIvYXdzEJr...",
  "Expiration":      "2026-04-08T12:00:00Z"
}
</code></code></pre><p>IMDS called STS internally when the role was attached, cached the result, and serves it via this endpoint. It also handles rotation &#8212; before <code>Expiration</code>, it calls STS again and updates the cache. The SDK just reads from IMDS. It never touches STS directly.</p><pre><code><code>SDK credential provider chain
        &#8595;
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/&lt;role&gt;
        &#8595;
IMDS returns (AccessKeyId, SecretAccessKey, Token)
        &#8595;
SDK uses these to sign the request
</code></code></pre><p><strong>IMDSv2 &#8212; closing an SSRF gap</strong></p><p>SSRF (Server-Side Request Forgery) is an attack where the attacker tricks the server itself into making a request to an internal resource the attacker can&#8217;t reach directly. A classic example &#8212; a web app has a &#8220;fetch this URL for me&#8221; feature. Intended for external URLs. But if an attacker passes <code>http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-role</code>, the server dutifully fetches it and hands back valid AWS credentials. The attacker never touched IMDS directly &#8212; they couldn&#8217;t, it&#8217;s only reachable from inside the instance. But the server could, and the server was fooled into doing it.</p><p>The original IMDS had exactly this problem. Any code running on the instance &#8212; including code exploiting an SSRF vulnerability in the application &#8212; could hit <code>169.254.169.254</code> and walk away with valid credentials.</p><p>IMDSv2 added a session-token requirement. Before fetching credentials, the caller must first do a PUT request to get a short-lived session token, then use that token in the GET:</p><pre><code><code># Step 1: get a session token
TOKEN=$(curl -X PUT \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" \
  http://169.254.169.254/latest/api/token)

# Step 2: use the token to fetch credentials
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-role
</code></code></pre><p>SSRF exploits typically follow HTTP redirects but can&#8217;t make a PUT with a custom header. That one constraint closes the attack vector. AWS SDKs handle IMDSv2 automatically &#8212; this is just what&#8217;s happening underneath.</p><p>This connects back to the same thesis from section 8 &#8212; the architecture is designed so there&#8217;s nothing static to steal. Even the path to credentials has a time-limited gate in front of it.</p><div><hr></div><h2>7. Cross-Account Access &#8212; The Same Story, One Level Up</h2><p>At work, I ran into this exact situation. A Step Function in one AWS account needed to trigger a Lambda in a completely different account. At first it felt like a special case &#8212; some obscure AWS feature I hadn&#8217;t seen before. Turns out it&#8217;s the same STS story, just applied across account boundaries.</p><p>Everything so far has been single-account. But real systems span multiple accounts. Production in one account, data in another, logging in a third.</p><p>By default, AWS accounts are completely isolated. A Lambda in Account A has zero access to anything in Account B.</p><p>The solution is the same STS story &#8212; just applied across account boundaries.</p><pre><code><code>Account A (123456789012) &#8212; Lambda lives here
Account B (999988887777) &#8212; S3 bucket lives here
</code></code></pre><p><strong>Setup: Two sides must agree</strong></p><p>In Account B, a role is created with a trust policy explicitly allowing Account A&#8217;s Lambda role to assume it:</p><pre><code><code>{
  "Principal": {
    "AWS": "arn:aws:iam::123456789012:role/my-lambda-role"
  },
  "Action": "sts:AssumeRole",
  "Condition": {
    "StringEquals": {
      "sts:ExternalId": "shared-secret-xyz"
    }
  }
}
</code></code></pre><p>In Account A, <code>my-lambda-role</code> is given permission to call <code>AssumeRole</code> on Account B&#8217;s role:</p><pre><code><code>{
  "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::999988887777:role/cross-account-role"
}
</code></code></pre><p>Both sides must agree. Account B&#8217;s role trusts Account A. Account A&#8217;s role is allowed to call AssumeRole on Account B&#8217;s role. Missing either side means access denied.</p><p><strong>The flow</strong></p><p>The Lambda code explicitly calls STS:</p><pre><code><code>result, err := stsClient.AssumeRole(context.TODO(), &amp;sts.AssumeRoleInput{
    RoleArn:         aws.String("arn:aws:iam::999988887777:role/cross-account-role"),
    RoleSessionName: aws.String("lambda-cross-account-session"),
    ExternalId:      aws.String("shared-secret-xyz"),
})
</code></code></pre><p>STS checks both sides, and if both agree, returns a second set of temporary credentials &#8212; scoped entirely to Account B.</p><p>The Lambda ends up holding two layers of credentials simultaneously:</p><pre><code><code>Layer 1 &#8212; Account A credentials (from runtime, automatic)
  &#8594; used for anything in Account A
  &#8594; used to call STS AssumeRole for Account B

Layer 2 &#8212; Account B credentials (from explicit AssumeRole call)
  &#8594; used only for resources in Account B
  &#8594; separate session, separate expiry, separate rotation
</code></code></pre><p>The mechanism is identical. The STS story just runs twice.</p><p><strong>Why ExternalId matters even more here</strong></p><p>In single-account, a compromised role damages only that account. In cross-account, if a third-party vendor&#8217;s AWS account is compromised, an attacker could try to assume the role from their account.</p><p><code>ExternalId</code> is the defence. Even with full access to the vendor&#8217;s account, an attacker doesn&#8217;t know the <code>ExternalId</code>. The AssumeRole call fails. This is the <strong>confused deputy problem</strong> &#8212; a service being tricked into acting on behalf of someone it shouldn&#8217;t trust.</p><div><hr></div><h2>8. The Thesis: No Passwords Anywhere</h2><p>Step back and look at the pattern.</p><p>A Lambda reads from S3. No password was configured. No key was hardcoded. No secret was shared between the two services. Yet the request was authenticated, authorized, and completed.</p><p>How?</p><ul><li><p>A role defined <em>who</em> can act and <em>what</em> they can do</p></li><li><p>STS issued short-lived credentials when needed</p></li><li><p>The credentials signed every request cryptographically</p></li><li><p>The signature was verified by recomputing it independently</p></li><li><p>When credentials expired, STS issued fresh ones automatically</p></li></ul><p>This is the same insight as TLS &#8212; except TLS applied it to network connections, and AWS IAM applies it to service-to-service calls inside a cloud platform.</p><p>In TLS: asymmetric cryptography establishes trust, then symmetric keys handle communication. Neither side stores a shared password.</p><p>In AWS IAM: roles define trust, STS issues tokens, SigV4 signs every request. No service stores another service&#8217;s password.</p><blockquote><p>The architecture is designed so that there is nothing static to steal.</p></blockquote><p>Leaked credentials expire. Intercepted requests can&#8217;t be replayed &#8212; the timestamp and scope are signed. A key valid for one service can&#8217;t be used against another.</p><p>The security property isn&#8217;t that secrets are well-hidden. It&#8217;s that secrets are designed to be short-lived and narrowly scoped from the start.</p><div><hr></div><h2>9. What&#8217;s Still Open</h2><p>This article followed a single request through IAM and STS. But there&#8217;s more to the full picture.</p><p><strong>Policy evaluation logic</strong> &#8212; when a request arrives at AWS, how exactly is the allow/deny decision made? Identity-based policies, resource-based policies, and Service Control Policies all interact. The logic is non-trivial and worth its own article. The mental model is similar to what OPA/Rego does &#8212; a policy engine evaluating structured rules against a request context &#8212; but AWS&#8217;s version is closed and AWS-specific.</p><p><strong>Federation</strong> &#8212; real humans in large orgs don&#8217;t get IAM users. They authenticate via SAML or OIDC against a corporate identity provider, and AWS maps that identity to a role via <code>AssumeRoleWithSAML</code> or <code>AssumeRoleWithWebIdentity</code>. The STS story is the same, the entry point is different.</p><p><strong>JWT and OAuth</strong> &#8212; how applications represent and delegate identity outside of AWS. Coming in the next part.</p><p>The chain of trust keeps extending. TLS secured the channel. IAM secured the caller. What comes next is how identity is represented and carried across the boundaries of individual systems.</p><div><hr></div><p><em>If something landed differently than expected, or if there&#8217;s a gap I didn&#8217;t cover, I&#8217;m curious to hear it.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://logs.craftedbyvishal.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Engineer&#8217;s Notebook! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[That Tiny 'i' in GiB Had Me Confused All Along]]></title><description><![CDATA[Why RAM is GiB, storage is GB, and network is Gigabit]]></description><link>https://logs.craftedbyvishal.dev/p/that-tiny-i-in-gib-had-me-confused</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/that-tiny-i-in-gib-had-me-confused</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Fri, 03 Apr 2026 19:56:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!f1ZR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!f1ZR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!f1ZR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 424w, https://substackcdn.com/image/fetch/$s_!f1ZR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 848w, https://substackcdn.com/image/fetch/$s_!f1ZR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 1272w, https://substackcdn.com/image/fetch/$s_!f1ZR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!f1ZR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png" width="1456" height="855" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:855,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:115802,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/193107586?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!f1ZR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 424w, https://substackcdn.com/image/fetch/$s_!f1ZR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 848w, https://substackcdn.com/image/fetch/$s_!f1ZR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 1272w, https://substackcdn.com/image/fetch/$s_!f1ZR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f866acf-3874-46a3-92c3-3338802f3ddb_1595x937.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h1>Why Does AWS Show GiB Instead of GB?</h1><p>I was browsing EC2 instance types on AWS when I noticed something odd.</p><p>Memory is listed in <strong>GiB</strong>. Storage is listed in <strong>GB</strong>. Network is in <strong>Gigabit</strong>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://logs.craftedbyvishal.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Engineer&#8217;s Notebook! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Three different units. Same console. All measuring data.</p><p>My first instinct was that AWS was being inconsistent. But the more I looked into it, the more I realised each one is deliberate, and understanding why took me through CPU architecture, a 1980s marketing decision, and an OS labeling bug the industry decided was too painful to fix.</p><div><hr></div><h2>Two Ways to Count to a Billion</h2><p>Everything starts here.</p><p>There are two competing definitions of &#8220;Gigabyte&#8221;, and they&#8217;ve been quietly causing confusion for decades.</p><p><strong>Base 10: the human way</strong></p><p>Powers of 10, like we learned in school.</p><pre><code><code>1 KB  = 1,000 bytes
1 MB  = 1,000,000 bytes
1 GB  = 1,000,000,000 bytes
1 TB  = 1,000,000,000,000 bytes
</code></code></pre><p><strong>Base 2: the computer way</strong></p><p>Powers of 2, because computers think in binary.</p><pre><code><code>1 KiB = 1,024 bytes
1 MiB = 1,048,576 bytes
1 GiB = 1,073,741,824 bytes
1 TiB = 1,099,511,627,776 bytes
</code></code></pre><p>1 GiB is about <strong>7.4% larger</strong> than 1 GB. Small at small scales, but it compounds fast.</p><p>And yes, that lowercase &#8220;i&#8221; sandwiched between G and B in GiB is genuinely easy to miss. Sit a foot further from the screen and it reads exactly like GB. I missed that &#8216;i&#8217; for longer than I&#8217;d like to admit.</p><p>The &#8220;bi&#8221; in KiB, MiB, GiB stands for <strong>binary</strong>. The IEC standardised these units in 1998 specifically to end the ambiguity. Before that, &#8220;KB&#8221; was used loosely for both 1,000 and 1,024 bytes depending on who was writing.</p><p>So why does AWS use GiB for memory but GB for storage?</p><p>It&#8217;s not inconsistency. It turns out each unit is accurately describing what&#8217;s actually there.</p><div><hr></div><h2>RAM Is Binary By Nature</h2><p>RAM isn&#8217;t just stored data, it&#8217;s <em>addressed</em> data.</p><p>Every byte in RAM has a unique memory address. The CPU locates it using binary addressing. A 32-bit address bus gives exactly <strong>2&#179;&#178;</strong> addressable locations, 4,294,967,296 bytes, or exactly 4 GiB. Not 4 GB. Not 4,000,000,000 bytes. The math doesn&#8217;t leave room for clean decimal boundaries.</p><p>This flows all the way down to how memory chips are manufactured. RAM comes in 256 MiB, 512 MiB, 1 GiB, 2 GiB, 4 GiB slots. A &#8220;1,000,000,000 byte&#8221; RAM stick doesn&#8217;t exist, it would break the addressing model.</p><p>So when AWS lists an EC2 instance as &#8220;8 GiB RAM,&#8221; it&#8217;s being precise. That&#8217;s 8 &#215; 2&#179;&#8304; bytes, because that&#8217;s what RAM physically is.</p><p>Engineers always knew this. When they said &#8220;1 GB of RAM&#8221; years ago, they meant 2&#179;&#8304; bytes, they were just using imprecise language before the right words existed. GiB makes the implicit explicit.</p><div><hr></div><h2>Storage Is Decimal By Choice</h2><p>Hard drives don&#8217;t have the same constraint.</p><p>A disk is a linear sequence of sectors. There&#8217;s no binary addressing requirement forcing capacity to be a power of 2, a drive can hold any number of bytes.</p><p>So when manufacturers were scaling up in the 1980s, they had a choice: count in base 2 or base 10?</p><p>They picked base 10. And the reason wasn&#8217;t engineering.</p><p><strong>It was marketing.</strong></p><p>Base 10 numbers are larger. A drive marketed as &#8220;10 MB&#8221; sounds better than the same drive marketed as &#8220;9.5 MiB.&#8221; As capacities scaled into gigabytes and terabytes, that gap grew from a minor rounding difference into a meaningfully larger-sounding number on the box.</p><p>The entire storage industry followed suit. It&#8217;s been base 10 ever since. When a manufacturer says &#8220;1 TB SSD,&#8221; they mean exactly 1,000,000,000,000 bytes, no tricks. That&#8217;s just their definition of TB.</p><p>AWS follows the same convention for EBS and S3.</p><div><hr></div><h2>So Where Does the Missing Storage Go?</h2><p>This is where it clicked for me.</p><p>The OS receives a drive with exactly 1,000,000,000,000 bytes. Internally, it counts in base 2, consistent with how memory addressing works across the whole system. So it calculates:</p><pre><code><code>1,000,000,000,000 &#247; 2&#8308;&#8304; = 0.9094 TiB
                          &#8776; 931 GiB
</code></code></pre><p>The number (931) is correct.</p><p>The label (&#8221;GB&#8221;) is wrong, it should say &#8220;GiB.&#8221;</p><p>But that label has been wrong for so long that fixing it would cause a different kind of chaos. Imagine waking up one morning and a &#8220;500 GB&#8221; drive suddenly reads &#8220;465 GiB.&#8221; The number dropped by 35. Most people would, reasonably but incorrectly, conclude the OS update deleted their data. Support forums melt down. Headlines scream <em>&#8220;New update shrinks your hard drive.&#8221;</em></p><p>So the industry made a quiet collective decision: leave the wrong label in place.</p><p>The confusion of an incorrect unit is less damaging than the panic of visibly shrinking numbers.</p><div><hr></div><h2>The One OS That Actually Fixed It</h2><p>In 2009, Apple shipped <strong>macOS Snow Leopard</strong> with a quiet but significant change, Finder switched to true base 10 reporting.</p><p>A 1 TB drive now showed as approximately 1 TB in Finder. The label and the math finally agreed.</p><p>The result? Mac users reported their drives had suddenly <em>gained</em> storage after the update.</p><p>Same drives. Same bytes. Just a different counting system.</p><p>Linux has been cleaning this up gradually. Modern tools like <code>df -h</code> and <code>lsblk</code> now correctly display GiB when calculating in base 2. It varies by distro, but the direction is toward precision.</p><p>Windows still shows base 2 numbers with &#8220;GB&#8221; labels.</p><div><hr></div><h2>And the &#8220;Gigabit&#8221; for Network?</h2><p>Network speeds add one more wrinkle, they&#8217;re measured in <strong>bits</strong>, not bytes.</p><p>When AWS says &#8220;Up to 5 Gigabit&#8221; network performance, that means 5,000,000,000 bits per second. To get usable throughput in bytes, divide by 8:</p><pre><code><code>5 Gbps &#247; 8 = 625 MB/s
</code></code></pre><p>Networking protocols historically transmitted data serially, one bit at a time over a wire. Measuring in bits was natural for the hardware, and it stuck. The same logic applies to ISP speeds, &#8220;100 Mbps broadband&#8221; gives about 12.5 MB/s of actual file transfer speed.</p><p>The &#8220;up to&#8221; also matters. It means burstable, the ceiling under ideal conditions, not a guaranteed baseline.</p><div><hr></div><h2>Putting It Together</h2><p>Once I understood this, the AWS console stopped looking inconsistent and started looking accurate:</p><p>What I see in AWS Unit Why EC2 memory: &#8220;8 GiB&#8221; GiB (base 2) RAM is binary by architecture EBS volume: &#8220;100 GB&#8221; GB (base 10) Storage follows manufacturer convention Network: &#8220;5 Gigabit&#8221; Gbps (base 10, bits) Networking measures in bits historically</p><p>And the mystery of a 1TB drive showing 931 GB? The OS counted correctly, in GiB, and then slapped the wrong label on it.</p><p>The storage was never missing.</p><p>The units were just speaking different languages.</p><div><hr></div><p><em>The Engineer&#8217;s Notebook. Things I learn, written down so I don&#8217;t forget them. If it helped, there&#8217;s more coming.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://logs.craftedbyvishal.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Engineer&#8217;s Notebook! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Building Trust on the Internet — Part 1: From First Principles to HTTPS]]></title><description><![CDATA[How does a browser securely talk to a server it has never met before over a network that anyone can intercept?]]></description><link>https://logs.craftedbyvishal.dev/p/building-trust-on-the-internet-part</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/building-trust-on-the-internet-part</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Thu, 26 Mar 2026 19:50:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kJrO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This question sits at the heart of HTTPS. There&#8217;s no shared secret, the network is untrusted, and attackers can read or modify packets. Yet, every day our browser establishes secure connections in milliseconds.</p><p>This article builds that system from first principles starting with basic cryptography and ending with modern TLS handshake.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://logs.craftedbyvishal.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Engineer&#8217;s Notebook! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h1>1. The Building Blocks</h1><h2>1.1 Symmetric Encryption - Fast but problematic</h2><p>Symmetric encryption uses the same key for encryption and decryption.</p><ul><li><p>Example algorithm: AES</p></li><li><p>Strength: extremely fast and is used for large data</p></li><li><p>Problem: the same key needs to be there with receiver. How to send the key securely?</p><ul><li><p>If we already had a secure way to share the key, why not just use that to send the data?</p></li></ul></li></ul><div><hr></div><h2>1.2 Asymmetric Encryption - Solving Trust, not speed</h2><p>Asymmetric encryption introduces key pairs:</p><ul><li><p>Public key (shared openly)</p></li><li><p>Private key (kept secret)</p></li></ul><p>Two different keys, different uses</p><ul><li><p>Encrypt using public key &#8594; Decrypt using private key</p><ul><li><p>Purpose: Confidentiality</p></li></ul></li><li><p>Sign using private key &#8594; Verify using public key (Digital Signatures)</p><ul><li><p>Purpose: Authenticity + Integrity</p></li></ul></li></ul><blockquote><p>Asymmetric encryption is not used for bulk data since it&#8217;s too slow. It&#8217;s used to establish trust and identity</p></blockquote><div><hr></div><h2>1.3 Hashing ensures integrity</h2><p>A hash function (like SHA-256)</p><ul><li><p>Takes in input of any size</p></li><li><p>produces fixed length output</p></li><li><p>is deterministic</p></li><li><p>is irreversible</p></li></ul><p><strong>Why Hashing?</strong></p><p>To verify data integrity</p><p>If  <code>HASH(data1) == HASH(data2)</code>, then <code>data1 == data2</code></p><div><hr></div><h2>2. Digital Signatures</h2><p>Digital signature combines Hashing and asymmetric encryption</p><p>Let&#8217;s define:</p><ul><li><p><code>HASH(k)</code> &#8594; hash of data <code>k</code></p></li><li><p><code>Sign(pvt, k)</code> &#8594; sign using private key</p></li><li><p><code>Verify(pub, sig, k)</code> &#8594; verify using public key</p></li></ul><div><hr></div><h3>Signing</h3><p><code>h = HASH(k)</code> , <code>sig = Sign(pvt, h)</code></p><div><hr></div><h3>Verification</h3><p><code>h1 = HASH(k), h2 = Verify(pub, sig)</code></p><p>if: <code>h1 == h2</code> &#8594; trusted source</p><div><hr></div><h2>What does this give us?</h2><ul><li><p>Integrity &#8594; data wasn&#8217;t changed while it reached destination</p></li><li><p>Authenticity &#8594; signed by owner of private key</p></li></ul><div><hr></div><h1>3. The Real Problem: Trust</h1><p>At this point, we can verify signatures, but the bigger problem is:</p><blockquote><p>How do we trust a public key?</p></blockquote><p>Anyone can generate a (pub, pvt) key pair</p><p>So how do we know if the key actually belongs to <code>example.com</code>?</p><div><hr></div><h1>4. Public Key Infrastructure (PKI)</h1><p>PKI solves the trust problem using hierarchy:</p><ul><li><p>Root Certificate Authority (RCA)</p></li><li><p>Intermediate Certificate Authority (ICA)</p></li><li><p>Web Servers (Websites)</p></li></ul><div><hr></div><h4>Key Concepts</h4><ul><li><p><strong>Certificate</strong> &#8594; binds identity + public key</p></li><li><p><strong>CA</strong> &#8594; entity that signs certificates</p><ul><li><p><strong>ICA</strong> &#8594; Intermediate Certificate Authority</p></li><li><p><strong>RCA</strong> &#8594; Root Certificate Authority</p></li></ul></li><li><p><strong>Chain of Trust</strong> &#8594; Layered verification</p></li></ul><div><hr></div><h1>How are certificates Issued</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kJrO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kJrO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 424w, https://substackcdn.com/image/fetch/$s_!kJrO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 848w, https://substackcdn.com/image/fetch/$s_!kJrO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 1272w, https://substackcdn.com/image/fetch/$s_!kJrO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kJrO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png" width="1456" height="957" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:957,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:336078,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/192237318?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kJrO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 424w, https://substackcdn.com/image/fetch/$s_!kJrO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 848w, https://substackcdn.com/image/fetch/$s_!kJrO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 1272w, https://substackcdn.com/image/fetch/$s_!kJrO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38df3e2e-0a74-403d-a89f-9c0a4fa9f661_2066x1358.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram above shows how websites prepare for building trust among browsers (or User Agents) to be able to securely connect to them. To understand the flow, read the diagram from right to left.</p><p>Lets dig in deeper:</p><ul><li><p><code>pub</code> &#8594; public key</p></li><li><p><code>pvt</code> &#8594; private key</p></li><li><p><code>HASH(k)</code> &#8594; hash function</p></li><li><p><code>Sign(pvt, k)</code> &#8594; signature</p></li></ul><div><hr></div><h3>Step 1 : ICA gets signed certificate from Root CA</h3><p>ICA generates:</p><pre><code><code>ICA.pub, ICA.pvt</code></code></pre><p>ICA sends a <strong>CSR (Certificate Signing Request)</strong> to Root CA.</p><p>Root CA:</p><pre><code><code>h = HASH(ICA.pub)
sig = Sign(RCA.pvt, h)</code></code></pre><p>Returns certificate:</p><pre><code><code>(ICA.pub, sig)</code></code></pre><div><hr></div><h3>Step 2: Server gets signed by ICA</h3><p>Web Server (WS) generates:</p><pre><code><code>WS.pub, WS.pvt</code></code></pre><p>Sends CSR to ICA.</p><p>ICA:</p><pre><code><code>h = HASH(WS.pub)
sig = Sign(ICA.pvt, h)</code></code></pre><p>Returns:</p><pre><code><code>WS Certificate:
  WS.pub
  Sign(ICA.pvt, HASH(WS.pub))

+ ICA Certificate:
  ICA.pub
  Sign(RCA.pvt, HASH(ICA.pub))</code></code></pre><div><hr></div><h3>Result: Chain of Trust</h3><pre><code><code>Root CA &#8594; ICA &#8594; Web Server</code></code></pre><div><hr></div><h3>6. How the Browser Verifies Trust</h3><p>The browser (or OS) already contains trusted <strong>Root CA public keys</strong>.</p><div><hr></div><h3>Verification Flow</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!X0bI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!X0bI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 424w, https://substackcdn.com/image/fetch/$s_!X0bI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 848w, https://substackcdn.com/image/fetch/$s_!X0bI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 1272w, https://substackcdn.com/image/fetch/$s_!X0bI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!X0bI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png" width="1456" height="1184" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1184,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:353642,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/192237318?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!X0bI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 424w, https://substackcdn.com/image/fetch/$s_!X0bI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 848w, https://substackcdn.com/image/fetch/$s_!X0bI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 1272w, https://substackcdn.com/image/fetch/$s_!X0bI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F854acca8-5e79-4d0c-8445-240cfa84bd54_2133x1734.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Step 1: Verify ICA</h4><pre><code><code>Verify(RCA.pub, ICA.signature)</code></code></pre><p>If valid &#8594; browser now trusts ICA</p><div><hr></div><h4>Step 2: Verify Server</h4><pre><code><code>Verify(ICA.pub, WS.signature)</code></code></pre><p>If valid &#8594; browser now trusts the web server (Bob)</p><div><hr></div><h3>7. TLS Handshake &#8212; Establishing a Secure Session</h3><p>Now that trust is established, we need a <strong>shared symmetric key</strong>.</p><div><hr></div><h4>Old Approach (RSA Key Exchange)</h4><ul><li><p>Client generates symmetric key</p></li><li><p>Encrypts with server public key and sends to server</p></li><li><p>Server decrypts using private key</p></li></ul><h3>Problem</h3><p>If attacker gets server private key later:<br>&#8594; past sessions can be decrypted</p><div><hr></div><h2>Modern Approach: Diffie-Hellman (ECDHE)</h2><p>Instead of sending the key, both sides <strong>derive it independently</strong>.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;(g^a\\ mod\\ p)^b&#8801;(g^a)^b\\ mod\\ p&quot;,&quot;id&quot;:&quot;GEOATNCLNF&quot;}" data-component-name="LatexBlockToDOM"></div><h2>Intuition</h2><ul><li><p>Both sides agree on <code>g</code> and <code>p</code></p></li><li><p>Client picks secret <code>a</code></p></li><li><p>Server picks secret <code>b</code></p></li></ul><p>Exchange:</p><pre><code><code>Client &#8594; g^a mod p
Server &#8594; g^b mod p</code></code></pre><p>Then:</p><pre><code><code>Client computes: (g^b)^a mod p
Server computes: (g^a)^b mod p</code></code></pre><p>Both get:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;g^{(ab)}\\ mod\\ p&quot;,&quot;id&quot;:&quot;CSXBKTPNBC&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Key Insight</h3><blockquote><p>The shared secret is never transmitted.</p></blockquote><h4>Why this is powerful</h4><ul><li><p>Provides <strong>Forward Secrecy</strong></p></li><li><p>Even if private key is compromised later:<br>&#8594; past sessions remain secure</p></li></ul><div><hr></div><h3>8. Final TLS Flow (Putting Everything Together)</h3><ol><li><p>Browser connects to server</p></li><li><p>Server sends certificate chain</p></li><li><p>Browser verifies:</p><ul><li><p>Root &#8594; ICA</p></li><li><p>ICA &#8594; Server</p></li></ul></li><li><p>Trust is established</p></li><li><p>Client &amp; server perform ECDHE key exchange</p></li><li><p>Shared symmetric key is derived</p></li><li><p>All further communication uses symmetric encryption (AES)</p></li></ol><div><hr></div><h3>9. Why This Design Works</h3><p>This system combines:</p><ul><li><p>Asymmetric crypto &#8594; identity &amp; trust</p></li><li><p>Hashing &#8594; integrity</p></li><li><p>Signatures &#8594; authenticity</p></li><li><p>PKI &#8594; global trust system</p></li><li><p>Diffie-Hellman &#8594; secure key exchange</p></li><li><p>Symmetric crypto &#8594; performance</p></li></ul><div><hr></div><h3>10. Final Mental Model</h3><p>Think of HTTPS as:</p><ul><li><p><strong>Certificates</strong> &#8594; prove identity</p></li><li><p><strong>Signatures</strong> &#8594; prove authenticity</p></li><li><p><strong>PKI</strong> &#8594; establish trust</p></li><li><p><strong>TLS handshake</strong> &#8594; agree on secret</p></li><li><p><strong>AES</strong> &#8594; secure communication</p></li></ul><div><hr></div><h3>Closing Thoughts</h3><p>What looks like a simple &#128274; in our browser is actually:</p><ul><li><p>a distributed trust system</p></li><li><p>built on cryptography</p></li><li><p>standardized through PKI</p></li><li><p>and optimized over decades</p></li></ul><p>In this article, we built the foundation:</p><ul><li><p><strong>Asymmetric cryptography</strong> &#8594; identity and signatures</p></li><li><p><strong>Hashing</strong> &#8594; integrity</p></li><li><p><strong>PKI</strong> &#8594; global trust</p></li><li><p><strong>TLS</strong> &#8594; secure communication channel</p></li></ul><p>But this only solves one part of the problem.</p><blockquote><p>TLS ensures we are talking securely to a server.<br>It does <strong>not</strong> answer who the user is or what they are allowed to do.</p></blockquote><div><hr></div><h3>What Comes Next</h3><p>Once a secure channel is established, modern systems need to answer two critical questions:</p><ol><li><p><strong>Who is the user?</strong> <em>(Authentication)</em></p></li><li><p><strong>What is the user allowed to do?</strong> <em>(Authorization)</em></p></li></ol><p>In the upcoming articles, we&#8217;ll build on this foundation and explore how real-world systems solve these problems:</p><ul><li><p><strong>JWT (JSON Web Tokens)</strong><br>&#8594; How identity is represented and verified across services</p></li><li><p><strong>JWKS (JSON Web Key Sets)</strong><br>&#8594; How public keys are distributed and rotated at scale</p></li><li><p><strong>OAuth</strong><br>&#8594; How applications securely delegate access</p></li><li><p><strong>Authorization Models</strong></p><ul><li><p>Role-Based Access Control (RBAC)</p></li><li><p>Attribute-Based Access Control (ABAC)</p></li><li><p>Policy-Based Access Control</p></li></ul></li><li><p><strong>Real-World Systems like AWS IAM</strong><br>&#8594; How these concepts come together in production systems</p></li></ul><div><hr></div><h3>The Bigger Picture</h3><p>If TLS answers:</p><blockquote><p><em>&#8220;Can we trust this server and communicate securely?&#8221;</em></p></blockquote><p>Then the rest of the system answers:</p><blockquote><p><em>&#8220;Can we trust this user, and what are they allowed to do?&#8221;</em></p></blockquote><div><hr></div><p>By the end of this series, we won&#8217;t just understand individual concepts like JWT or OAuth in isolation&#8212;we&#8217;ll understand how they <strong>fit together into a complete, real-world authentication and authorization system</strong>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://logs.craftedbyvishal.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Engineer&#8217;s Notebook! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Concurrency in Go Feels More Like Organizing People Than Using Threads]]></title><description><![CDATA[A human way to understand go-routines, scheduling, and work stealing]]></description><link>https://logs.craftedbyvishal.dev/p/concurrency-in-go-feels-more-like</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/concurrency-in-go-feels-more-like</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Thu, 25 Dec 2025 04:30:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!grHE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before talking about go-routines, channels, or worker pools, we must ask a basic question:</p><blockquote><p><strong>Can this problem be solved serially in a simple, obvious way?</strong></p></blockquote><p>Take the counting problem.</p><p><code>Count from 1 to 10,000</code></p><p>A single loop does this perfectly fine.</p><ul><li><p>Easy to read</p></li><li><p>Easy to reason about</p></li><li><p>Zero coordination cost</p></li><li><p>No overhead</p></li></ul><p>If your program only needs to count <strong>10,000 numbers</strong>, introducing concurrency would be like hiring <strong>8 people to count 10 pages</strong> &#8212; you&#8217;d spend more time coordinating than counting.</p><blockquote><p>This way of thinking applies not just to abstract numbers, but to how humans naturally organize work at scale.</p></blockquote><div><hr></div><h2>When Concurrency Becomes a Question</h2><p>Now change the problem slightly:</p><p><code>Count from 1 to 1,000,000,000</code></p><p>Suddenly, the serial solution has a cost:</p><ul><li><p>The CPU is busy for a long time</p></li><li><p>Other work might be blocked</p></li><li><p>The program becomes <em>unavailable</em> for extended periods</p></li></ul><p>This is the key transition point.</p><blockquote><p><strong>We don&#8217;t introduce concurrency because the task is big.<br>We introduce concurrency because the serial execution makes something unavailable.</strong></p></blockquote><p>I realized that the problem of counting currency notes maps extremely well to how concurrency in Go &#8212; and even the Go scheduler &#8212; is designed.</p><p>If we think about the currency note counting problem, when there are only a few bundles, we don&#8217;t need to think much &#8212; a single person and a single counting table are more than sufficient.</p><p>But if we are supplied with a huge number of currency note bundles &#8212; say, suitcases full of them &#8212; counting alone no longer makes sense, even if the person is perfectly capable.</p><p>So the key takeaway here is this: <strong>unless the scale of the problem is large enough, we don&#8217;t need to jump into thinking in terms of concurrency.</strong></p><p>Keeping that in mind, let&#8217;s understand concurrency design by co-relating it with the currency note counting problem.</p><div><hr></div><h2>Counting Hall Story</h2><h3>Deciding on the Cast</h3><ul><li><p>The hall has a limited number of counting tables (say 8).</p></li><li><p>The hall receives many currency note bundles of different denominations.</p></li><li><p>There are many people who know how to count notes &#8212; <strong>workers</strong>.</p></li><li><p><strong>Condition</strong>: a worker can count only when they acquire both a table and a bundle.</p></li><li><p>Sometimes, we deliberately assign <strong>more workers than tables</strong>.</p><ul><li><p>This ensures that tables never sit idle &#8212; if someone finishes early or pauses, another worker can immediately take the chair.</p></li></ul></li><li><p>Even if a worker has been waiting near one table, they may notice another table becoming free and rush to occupy it.</p></li><li><p>In addition, we introduce two special roles:</p><ul><li><p><strong>Producers</strong>, who distribute bundles to workers</p></li><li><p><strong>Accumulators</strong>, who collect and aggregate results</p></li></ul></li></ul><div><hr></div><h3>Deciding on the Implementation</h3><ul><li><p>Conceptually, the implementation consists of two pipes:</p><ul><li><p>a <strong>job pipe</strong></p></li><li><p>a <strong>result pipe</strong></p></li></ul></li><li><p>This separation ensures unidirectional flow and clear ownership of responsibilities.</p></li><li><p>Every worker is connected to:</p><ul><li><p>the <strong>receiving end</strong> of the job pipe</p><ul><li><p>when a worker acquires a table, they <strong>receive</strong> a currency bundle from this job pipe</p></li></ul></li><li><p>the <strong>sending end</strong> of the result pipe</p><ul><li><p>after counting the bundle, the worker prepares a result chit and <strong>sends</strong> it through the result pipe to the accumulator</p></li></ul></li></ul></li><li><p>The producer is connected to the <strong>sending end</strong> of the job pipe.</p></li><li><p>The accumulator is connected to the <strong>receiving end</strong> of the result pipe.</p></li></ul><div><hr></div><h2>Co-relating with Go</h2><h3>The Corresponding Cast</h3><p>If we map this to a Go implementation, the cast looks like this:</p><ul><li><p>The machine has a limited number of CPU cores (say 8). </p><ul><li><p>The machine maps to the hall and each CPU maps to table with chair</p></li></ul></li><li><p>We have a set of note bundles of different denominations.</p></li><li><p>Each <strong>worker go-routine</strong>, when in the <strong>Running</strong> state, is capable of counting notes from a given bundle when assigned CPU time.</p><ul><li><p>Each worker go-routine maps to a worker (human knowing counting)</p></li></ul></li><li><p><strong>Condition</strong>: a go-routine can count only when it is given both a CPU and a bundle.</p></li><li><p>The <strong>extra</strong> worker go-routines remain in the <strong>Runnable / Ready</strong> state &#8212; not counting yet, but ready to run and waiting near a CPU.</p></li><li><p>Conceptually, the behavior of acquiring the first available chair maps closely to Go&#8217;s <strong>work-stealing scheduler</strong>, where idle processors pull runnable go-routines from other queues to keep CPUs busy.</p><ul><li><p>Can read more about work-stealing in Go&#8217;s scheduler <a href="https://go.dev/src/runtime/proc.go">here</a>.</p></li></ul></li><li><p>The <strong>producer</strong> and <strong>accumulator</strong> go-routines are special go-routines that do not count notes themselves, but instead facilitate work (bundle) distribution and result aggregation.</p></li></ul><div><hr></div><h3>The Corresponding Implementation</h3><ul><li><p>The pipes are implemented using Go channels.</p></li></ul><p>Now, if we look at the following Go implementation, the analogy becomes much clearer.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!grHE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!grHE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 424w, https://substackcdn.com/image/fetch/$s_!grHE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 848w, https://substackcdn.com/image/fetch/$s_!grHE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 1272w, https://substackcdn.com/image/fetch/$s_!grHE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!grHE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png" width="918" height="2096" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2096,&quot;width&quot;:918,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:336067,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182253667?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!grHE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 424w, https://substackcdn.com/image/fetch/$s_!grHE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 848w, https://substackcdn.com/image/fetch/$s_!grHE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 1272w, https://substackcdn.com/image/fetch/$s_!grHE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff77dcb77-12d8-414f-938b-eaf4d80f04b1_918x2096.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Output</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JzWv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JzWv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 424w, https://substackcdn.com/image/fetch/$s_!JzWv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 848w, https://substackcdn.com/image/fetch/$s_!JzWv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 1272w, https://substackcdn.com/image/fetch/$s_!JzWv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JzWv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png" width="582" height="383" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70c15032-c026-4807-bff3-d37b48897b6d_582x383.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:383,&quot;width&quot;:582,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:88512,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182253667?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JzWv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 424w, https://substackcdn.com/image/fetch/$s_!JzWv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 848w, https://substackcdn.com/image/fetch/$s_!JzWv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 1272w, https://substackcdn.com/image/fetch/$s_!JzWv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70c15032-c026-4807-bff3-d37b48897b6d_582x383.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h3>Co-relation</h3><ul><li><p>Each <strong>worker go-routine</strong> counts a bundle of notes <strong>received</strong> through the <strong>jobs channel (pipe)</strong> from the <strong>producer go-routine</strong> and <strong>sends</strong> the result through the <strong>results channel (pipe)</strong>.</p><p>This behavior is enforced by the signature of the <code>worker</code> function:</p><ul><li><p><code>jobs</code> parameter has the type <code>&lt;-chan Bundle</code>, a <strong>read-only</strong> channel, which <strong>explicitly restricts</strong> the worker to only <em>receiving</em> from the <code>jobs</code> channel and not sending into it.</p></li><li><p><code>results</code> parameter has the type <code>chan&lt;- CountResult</code>, a <strong>send-only</strong> channel, which <strong>explicitly restricts</strong> the worker to only <em>sending</em> results into the <code>results</code> channel and not receiving from it.</p></li></ul><blockquote><p>This makes the worker&#8217;s role unambiguous: it consumes work from one pipe and produces results into another.</p></blockquote></li><li><p>The <strong>main go-routine</strong> acts as the <strong>accumulator</strong>, holding the <strong>receiving end of the results channel</strong>, into which all worker go-routines send their results.</p></li><li><p>Each CPU core acts as a counting table.</p></li><li><p>The <code>[]Bundle{}</code> slice represents the collection of bundles to be counted.</p></li><li><p><code>CountResult</code> represents the result <strong>chit</strong> prepared by each worker and sent through the <strong>results channel (pipe)</strong> to the accumulator.</p></li></ul><div><hr></div><h2>Conclusion</h2><p>In the end, I don&#8217;t want us to lose sight of the <strong>availability over speed</strong> perspective on concurrency.</p><p>Throughout the counting hall story, the goal was never to make a single person count faster.  </p><p>The goal was to make sure that <em><strong>counting never stops</strong></em>.</p><p>We added more workers not to increase individual speed, but to ensure that:</p><ul><li><p>when one worker pauses, another can immediately take a table,</p></li><li><p>when one table becomes free, some worker is always ready to occupy it,</p></li><li><p>and no table ever sits idle while work is waiting.</p></li></ul><p>That is exactly what Go&#8217;s concurrency model optimizes for.</p><p>Go-routines are cheap to create not so that they all run at once, but so that there is <strong>always</strong> work ready when CPU time becomes available.  </p><p><strong>Runnable</strong> go-routines exist so that the system can immediately make progress when capacity frees up.  </p><p><strong>Work stealing</strong> exists so that idle CPUs don&#8217;t wait while work is stuck elsewhere.</p><p>All of this is about <strong>keeping the system responsive and productive</strong>, even as the amount of work scales.</p><p>That brings us back to the core idea:</p><blockquote><p><strong>Concurrency is not about making one person faster.</strong></p><p><strong>It&#8217;s about keeping the hall productive even when work scales.</strong></p><p>And that, more than raw performance, is what good concurrency design is about.</p></blockquote><p>At its core, good concurrency prioritizes:</p><ul><li><p><strong>availability</strong> &#8212; the system is always ready to do useful work,</p></li><li><p><strong>clarity</strong> &#8212; each component has a well-defined role,</p></li><li><p><strong>structure</strong> &#8212; work flows in one direction with clear coordination,</p></li><li><p>and <strong>human-like organization</strong> &#8212; because that&#8217;s how large systems naturally scale.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[I Thought Go Concurrency Was About Speed — I Was Wrong]]></title><description><![CDATA[Concurrency made my server more available, not faster.]]></description><link>https://logs.craftedbyvishal.dev/p/i-thought-go-concurrency-was-about</link><guid isPermaLink="false">https://logs.craftedbyvishal.dev/p/i-thought-go-concurrency-was-about</guid><dc:creator><![CDATA[Vishal Govind]]></dc:creator><pubDate>Sun, 21 Dec 2025 07:36:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iVDN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I used to think Go concurrency was about making things faster.</p><p>Then I wrote a tiny TCP server.</p><p>What broke wasn&#8217;t performance &#8212; it was <strong>availability</strong>.</p><p>A single blocking <code>Accept()</code> call meant:</p><ul><li><p>one client connected</p></li><li><p>everyone else waited</p></li></ul><p>Adding a goroutine didn&#8217;t make the server faster.<br>It made the server <strong>usable</strong>.</p><p>That&#8217;s when concurrency stopped feeling like a language feature and started feeling like a design decision.</p><p>I wrote about this realization while evolving a raw TCP server in Go &#128071;</p><p>I used to think Go concurrency was about <strong>goroutines and channels</strong>.</p><p>You know the advice:</p><blockquote><p>&#8220;Just put it in a goroutine.&#8221;</p></blockquote><p>But none of that really <em>clicked</em> for me until I wrote a very small, very boring TCP server &#8212; and watched it behave in ways I didn&#8217;t expect.</p><p>This post is about how I slowly evolved a raw TCP server in Go and discovered what concurrency <em>actually</em> means &#8212; not as a concept, but as behavior.</p><div><hr></div><h2>Stage 1: The simplest possible TCP server</h2><p>I started with the smallest thing that could work:</p><ul><li><p>listen on a TCP port</p></li><li><p>accept one connection</p></li><li><p>print who connected</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iVDN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iVDN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 424w, https://substackcdn.com/image/fetch/$s_!iVDN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 848w, https://substackcdn.com/image/fetch/$s_!iVDN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 1272w, https://substackcdn.com/image/fetch/$s_!iVDN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iVDN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png" width="1085" height="756" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:756,&quot;width&quot;:1085,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:99282,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iVDN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 424w, https://substackcdn.com/image/fetch/$s_!iVDN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 848w, https://substackcdn.com/image/fetch/$s_!iVDN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 1272w, https://substackcdn.com/image/fetch/$s_!iVDN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2172776-f3e1-4066-a7dc-da8219115f60_1085x756.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Output</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!C-Cg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!C-Cg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 424w, https://substackcdn.com/image/fetch/$s_!C-Cg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 848w, https://substackcdn.com/image/fetch/$s_!C-Cg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 1272w, https://substackcdn.com/image/fetch/$s_!C-Cg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!C-Cg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png" width="733" height="294" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:294,&quot;width&quot;:733,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35733,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!C-Cg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 424w, https://substackcdn.com/image/fetch/$s_!C-Cg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 848w, https://substackcdn.com/image/fetch/$s_!C-Cg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 1272w, https://substackcdn.com/image/fetch/$s_!C-Cg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdee1f363-1a77-4ec3-b9be-746ce4ffab65_733x294.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What this taught me</h3><p>Once a client connected, the server stopped making progress.</p><p>No new connections. No errors. Just&#8230; stuck.</p><p>That&#8217;s when it hit me:</p><blockquote><p><code>Accept()</code> is a <strong>blocking call</strong>.<br>The server wasn&#8217;t idle &#8212; it was blocked.</p></blockquote><p>Concurrency hadn&#8217;t entered the picture yet, but the <em>problem</em> already existed.</p><div><hr></div><h2>Stage 2: Reading input &#8212; still single-client</h2><p>Next, I tried reading from the connection.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!t9bb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!t9bb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 424w, https://substackcdn.com/image/fetch/$s_!t9bb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 848w, https://substackcdn.com/image/fetch/$s_!t9bb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 1272w, https://substackcdn.com/image/fetch/$s_!t9bb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!t9bb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png" width="1165" height="1055" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1055,&quot;width&quot;:1165,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:140895,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!t9bb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 424w, https://substackcdn.com/image/fetch/$s_!t9bb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 848w, https://substackcdn.com/image/fetch/$s_!t9bb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 1272w, https://substackcdn.com/image/fetch/$s_!t9bb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac8baf65-811e-47fe-a8a5-197aec85a9b7_1165x1055.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Output</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ide1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ide1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 424w, https://substackcdn.com/image/fetch/$s_!Ide1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 848w, https://substackcdn.com/image/fetch/$s_!Ide1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 1272w, https://substackcdn.com/image/fetch/$s_!Ide1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ide1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png" width="897" height="539" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:539,&quot;width&quot;:897,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:56846,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ide1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 424w, https://substackcdn.com/image/fetch/$s_!Ide1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 848w, https://substackcdn.com/image/fetch/$s_!Ide1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 1272w, https://substackcdn.com/image/fetch/$s_!Ide1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe793a6ec-8d60-482a-a0ab-514dc0520f99_897x539.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What this taught me</h3><p>The server now <em>did something useful</em>.</p><p>But it still handled <strong>only one client</strong>.</p><p>If a second client tried to connect while the first was active, it had to wait.</p><p>The server was alive &#8212; but unavailable.</p><p>That distinction mattered more than I expected.</p><div><hr></div><h2>Stage 3: Accepting multiple connections&#8230; serially</h2><p>My next instinct was obvious:</p><pre><code>for {
    conn, _ := lis.Accept()
    handle(conn)
}</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vOS8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vOS8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 424w, https://substackcdn.com/image/fetch/$s_!vOS8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 848w, https://substackcdn.com/image/fetch/$s_!vOS8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 1272w, https://substackcdn.com/image/fetch/$s_!vOS8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vOS8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png" width="1121" height="1137" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1137,&quot;width&quot;:1121,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:147291,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vOS8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 424w, https://substackcdn.com/image/fetch/$s_!vOS8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 848w, https://substackcdn.com/image/fetch/$s_!vOS8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 1272w, https://substackcdn.com/image/fetch/$s_!vOS8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dee60f2-7402-43a7-8579-e7049cea62e5_1121x1137.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Surely this meant multiple clients, right?</p><h3>Output</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FucY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FucY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 424w, https://substackcdn.com/image/fetch/$s_!FucY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 848w, https://substackcdn.com/image/fetch/$s_!FucY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 1272w, https://substackcdn.com/image/fetch/$s_!FucY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FucY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png" width="1121" height="702" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:702,&quot;width&quot;:1121,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:97948,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FucY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 424w, https://substackcdn.com/image/fetch/$s_!FucY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 848w, https://substackcdn.com/image/fetch/$s_!FucY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 1272w, https://substackcdn.com/image/fetch/$s_!FucY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ef2cf19-e520-4a95-8edd-61ad38c2ba23_1121x702.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What this taught me</h3><p>Even with a loop, clients were handled <strong>one after another</strong>.</p><p>The server <em>could</em> accept multiple connections &#8212; just not at the same time.</p><p>This is when I wrote this line in my notes:</p><blockquote><p>Accepting connections is easy.<br>Handling them concurrently is the real problem.</p></blockquote><p>That sentence stayed with me.</p><div><hr></div><h2>Stage 4: Goroutine per connection &#8212; the first breakthrough</h2><p>Then came the smallest change that changed everything:</p><pre><code><code>for {
    conn, _ := lis.Accept()
    go handleConn(conn)
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DutW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DutW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 424w, https://substackcdn.com/image/fetch/$s_!DutW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 848w, https://substackcdn.com/image/fetch/$s_!DutW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 1272w, https://substackcdn.com/image/fetch/$s_!DutW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DutW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png" width="1085" height="1246" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1246,&quot;width&quot;:1085,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:164310,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DutW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 424w, https://substackcdn.com/image/fetch/$s_!DutW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 848w, https://substackcdn.com/image/fetch/$s_!DutW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 1272w, https://substackcdn.com/image/fetch/$s_!DutW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ee0a6be-301f-4337-a57f-629553af41a6_1085x1246.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Output</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5Dt1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5Dt1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 424w, https://substackcdn.com/image/fetch/$s_!5Dt1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 848w, https://substackcdn.com/image/fetch/$s_!5Dt1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 1272w, https://substackcdn.com/image/fetch/$s_!5Dt1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5Dt1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png" width="1085" height="702" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:702,&quot;width&quot;:1085,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:105900,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://vishalageek.substack.com/i/182219420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5Dt1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 424w, https://substackcdn.com/image/fetch/$s_!5Dt1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 848w, https://substackcdn.com/image/fetch/$s_!5Dt1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 1272w, https://substackcdn.com/image/fetch/$s_!5Dt1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff673fe11-f25d-4d75-b76f-4e5cfb36d16f_1085x702.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Messages from different clients started <strong>interleaving naturally</strong>.</p><h3>What this taught me</h3><p>This wasn&#8217;t about speed.</p><p>It was about <strong>availability</strong>.</p><p>Each connection now had its own execution path.<br>No client blocked another.</p><p>For the first time, goroutines stopped feeling like magic and started feeling <em>necessary</em>.</p><div><hr></div><h2>What this taught me about concurrency</h2><p>Before writing this TCP server, I thought concurrency in Go was mainly about:</p><ul><li><p>doing more work in parallel</p></li><li><p>improving throughput</p></li><li><p>&#8220;making things faster&#8221;</p></li></ul><p>This exercise completely changed that.</p><p>The biggest improvement didn&#8217;t come from optimization.<br>It came from <strong>availability</strong>.</p><p>Before goroutines:</p><ul><li><p>one blocking <code>Accept()</code></p></li><li><p>one blocking read</p></li><li><p>one client could make the entire server unavailable</p></li></ul><p>The server wasn&#8217;t broken.<br>It was just <em>unreachable</em>.</p><p>Adding a goroutine per connection didn&#8217;t make the server faster.<br>It made the server <strong>usable</strong>.</p><p>Multiple clients could connect.<br>One slow or silent client no longer blocked everyone else.</p><p>That&#8217;s when concurrency stopped feeling like a language feature and started feeling like a <strong>design decision</strong>.</p><div><hr></div><h2>The mental shift that finally clicked for me</h2><p>I used to ask:</p><blockquote><p>&#8220;How do I make this code concurrent?&#8221;</p></blockquote><p>Now I ask:</p><blockquote><p>&#8220;What is blocking, and who does it make unavailable?&#8221;</p></blockquote><p>Once I started thinking in those terms:</p><ul><li><p>goroutines felt obvious</p></li><li><p>concurrency felt justified</p></li><li><p>the design choices made sense</p></li></ul><p>Go didn&#8217;t &#8220;add concurrency&#8221; to my server.<br>It removed unnecessary <strong>unavailability</strong>.</p><p>That distinction changed how I look at backend systems.</p><div><hr></div><h2>Where this goes next</h2><p>This server is concurrent &#8212; but still incomplete.</p><p>It doesn&#8217;t coordinate clients.<br>It doesn&#8217;t handle slow consumers.<br>It doesn&#8217;t shut down gracefully.</p><p>In the next part, I want to explore what happens <strong>after</strong> availability:</p><ul><li><p>how concurrent goroutines coordinate</p></li><li><p>why channels change the design</p></li><li><p>and how ownership keeps systems sane</p></li></ul><p>But this first realization deserved its own space.</p><div><hr></div><p>If this post saved you even one hour of confusion, it did its job.</p>]]></content:encoded></item></channel></rss>