<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Http on Shane&apos;s Personal Blog</title><description>Recent content in Http on Shane&apos;s Personal Blog</description><link>https://shanechang.com/tags/http/</link><language>en-us</language><lastBuildDate>Wed, 13 May 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/tags/http/index.xml" rel="self" type="application/rss+xml"/><item><title>The Art of Buying a Toaster</title><link>https://shanechang.com/p/networking-journey-buying-a-toaster/</link><guid isPermaLink="true">https://shanechang.com/p/networking-journey-buying-a-toaster/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.CGOZWnV8_Z1qvbwJ.webp&quot; alt=&quot;Featured image of post The Art of Buying a Toaster&quot; /&gt;&lt;p&gt;If you’re new, the story so far — in &lt;a href=&quot;../networking-journey-portainer-and-switches/&quot;&gt;What a Switch Actually Does&lt;/a&gt; I admitted I’d been faking my way through networking for years, dug into what a switch actually does, and finally understood why a listener bound to &lt;code&gt;127.0.0.1&lt;/code&gt; is invisible to a container connecting via &lt;code&gt;10.88.0.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For everyone else: this post is a slight detour, and on purpose.&lt;/p&gt;
&lt;h2 id=&quot;the-embarrassment&quot;&gt;The Embarrassment&lt;/h2&gt;
&lt;p&gt;Somewhere in the middle of writing post 1, I tried to explain what I was learning to a friend who doesn’t write code. We were having dinner. She’d politely asked what I’d been geeking out about all week.&lt;/p&gt;
&lt;p&gt;I made it about three sentences in before her eyes did the thing.&lt;/p&gt;
&lt;p&gt;You know the thing. The micro-glaze. The “I love you and I am still here, but I am no longer parsing English.” I’d already said “TCP” and “the kernel” and “bridge interface” and I was about to say “MAC address,” and I caught myself and stopped.&lt;/p&gt;
&lt;p&gt;Here’s what hit me. I could now explain a switch. I could explain a virtual bridge. I could explain why my Portainer container couldn’t see my SSH tunnel. But if my friend had asked me — &lt;em&gt;what actually happens, from start to finish, when she clicks Buy on a website?&lt;/em&gt; — I would have started flailing. I had pieces. I didn’t have a story.&lt;/p&gt;
&lt;p&gt;So I sat down to write one. A real story. With characters. With a setting. With stakes. No jargon. The kind of story I could tell her over the rest of dinner.&lt;/p&gt;
&lt;p&gt;The decoder ring — what every character “really” is in computer terms — is at the end. Read the story first. If you’ve never thought about how a website actually works, that’s perfect. Especially then.&lt;/p&gt;
&lt;h2 id=&quot;the-cast&quot;&gt;The Cast&lt;/h2&gt;
&lt;p&gt;It helps to know the small cast before we begin. They’re going to come and go quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jack.&lt;/strong&gt; A man at home, shopping online. He wants a toaster. This is his entire characterisation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Browser-Bot.&lt;/strong&gt; Jack’s little assistant. Lives in his laptop. Writes notes, packs them into parcels, hands them to couriers. Faithful, fussy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Sea of Couriers.&lt;/strong&gt; A vast, restless relay of runners between Jack’s home and the shop. Each one knows only the next runner on the route. They never read the notes; they only pass them along.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Front Gate.&lt;/strong&gt; A small, stoic doorperson at the entrance of an office building somewhere far from Jack. Its entire job is to accept parcels addressed to this building and refuse all others. It cannot read.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mama Kernel.&lt;/strong&gt; The office manager. Knows every door, every desk, every resident. She sits in a back office with three things on her table: a directory of every department in the building, a rulebook for rewriting addresses, and an enormous ledger where she writes down every parcel that came in and what she did to it. Calm, unflappable, has seen everything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-Clerk and Post-Clerk.&lt;/strong&gt; Two assistants who work in pairs. The Pre-Clerk meets every incoming parcel at the door and sometimes rewrites the address on the outside. The Post-Clerk does the same for outgoing parcels — and he keeps a copy of every rewrite his colleague made, so he can put things back exactly as they were on the way out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Hallway Concierge.&lt;/strong&gt; Runs the inside corridor of the building. Knows every resident by sight. His only superpower is delivering a parcel from one apartment door to another — never up, never down, just sideways along his hallway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frieda the Frontend.&lt;/strong&gt; A friendly resident on the corridor. Apartment 10. She runs a kind of front desk: she greets every visitor’s parcel, looks at what it’s asking for, and either answers it herself or politely forwards it down the hall to whoever can.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Brutus the Backend.&lt;/strong&gt; Frieda’s neighbour. Apartment 11. Gruff bookkeeper. Will not talk to outsiders. Will only accept parcels handed to him by Frieda. Does the actual work — opening ledgers, debiting accounts, marking inventory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Parcels.&lt;/strong&gt; Every message in this story is shaped like a Russian nesting doll. A letter inside an envelope inside a packet inside a delivery satchel. Each layer is addressed to a different recipient, for a different leg of the journey. We will watch them get unwrapped and rewrapped many times.&lt;/p&gt;
&lt;p&gt;That is everyone. Now to the story.&lt;/p&gt;
&lt;h2 id=&quot;act-one--the-click&quot;&gt;Act One — The Click&lt;/h2&gt;
&lt;p&gt;Jack wants a toaster.&lt;/p&gt;
&lt;p&gt;He has, in fact, wanted a toaster for several weeks. The old one finally died last Sunday, dramatically, in a small puff of smoke that made the cat leave the room. He has been putting this off because online shopping always feels like more of a commitment than it ought to. But today is the day.&lt;/p&gt;
&lt;p&gt;He scrolls. He picks one. He clicks &lt;strong&gt;Buy&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the corner of Jack’s laptop, Browser-Bot springs into action. He’s a tidy little creature, all bow-tie and clipboard. He scribbles a short note — barely a sentence: &lt;em&gt;“this customer would like to buy one toaster”&lt;/em&gt; — and begins to wrap it for the road.&lt;/p&gt;
&lt;p&gt;First, the note goes inside a small envelope, addressed in Browser-Bot’s neat handwriting. Then that envelope goes inside a larger packet, with a different address on it — the address of the shop, far away. Then the whole packet goes inside an outer satchel, with yet a third address on it — this one for the very first relay courier waiting outside Jack’s front door.&lt;/p&gt;
&lt;p&gt;Three layers. Three addresses. Each one meant for a different leg of the journey. Browser-Bot has done this a thousand times today already. He hands the satchel out the door.&lt;/p&gt;
&lt;h2 id=&quot;act-two--across-the-sea-of-couriers&quot;&gt;Act Two — Across the Sea of Couriers&lt;/h2&gt;
&lt;p&gt;A relay runner is already waiting. She takes the satchel, glances at the outer address, and runs.&lt;/p&gt;
&lt;p&gt;She runs to the next runner, who tears off the outer wrapping — it had served its purpose — looks at the next address inside, wraps that inner packet in &lt;em&gt;her own&lt;/em&gt; fresh satchel addressed to the next relay over, and hands it on.&lt;/p&gt;
&lt;p&gt;This goes on for a long time.&lt;/p&gt;
&lt;p&gt;Every runner along the way does the same thing: tear off the outer wrapping that was only meant for the last leg, look at the address on what’s inside, wrap it fresh for the next leg, send it along. The inner packet — the one with the actual shop’s address on it — never gets opened. It is too sacred. It is &lt;em&gt;meant&lt;/em&gt; to travel.&lt;/p&gt;
&lt;p&gt;If you could speed up time and watch from above, you’d see a glowing point hopping from runner to runner across a continent, leaving a trail of discarded satchels in its wake. The note inside has not moved relative to its packet. The packet has not moved relative to itself. Only the outermost wrapper keeps getting reborn.&lt;/p&gt;
&lt;p&gt;Eventually, after what is in human terms a tiny fraction of a second, the packet reaches the office building it was always destined for. The last relay courier hands it across to the building’s Front Gate, gives a half-salute, and runs off into the night.&lt;/p&gt;
&lt;h2 id=&quot;act-three--the-onion-unwrapped&quot;&gt;Act Three — The Onion Unwrapped&lt;/h2&gt;
&lt;p&gt;The Front Gate is uninterested in the contents of the satchel. The Front Gate is only interested in one thing: the name written on the outermost layer. It is the building’s name. The Gate accepts the parcel. The Gate’s job is done.&lt;/p&gt;
&lt;p&gt;The parcel is conveyed into the back office, where Mama Kernel is waiting.&lt;/p&gt;
&lt;p&gt;She picks it up gently. She tears off the outer satchel — that was only ever for the last leg of the road. She lays the inner packet on her table. The address on &lt;em&gt;that&lt;/em&gt; is the building’s, all right, but written in a more permanent kind of way. Good. For us.&lt;/p&gt;
&lt;p&gt;She is about to unwrap the next layer when the Pre-Clerk appears at her elbow.&lt;/p&gt;
&lt;p&gt;“This one’s for Apartment 10,” he says. “Sales inquiry. Frieda’s department.”&lt;/p&gt;
&lt;p&gt;Mama Kernel nods. The Pre-Clerk takes a pencil and, very gently, edits the address on the packet — changing &lt;em&gt;the building’s&lt;/em&gt; name to &lt;em&gt;Frieda’s exact apartment number&lt;/em&gt;. He turns to his ledger and notes carefully: &lt;em&gt;Parcel #8472, originally addressed to the building, redirected to Apartment 10. Remember to reverse this on the way back.&lt;/em&gt; He underlines &lt;em&gt;remember&lt;/em&gt; three times. He is very serious about his job.&lt;/p&gt;
&lt;p&gt;Mama Kernel takes the relabelled packet and consults her directory. Apartment 10 — that’s down the corridor, the Concierge’s territory. She wraps the packet one more time, in a small in-building courier sleeve addressed by name to Frieda herself, and hands it through her window to the Hallway Concierge.&lt;/p&gt;
&lt;h2 id=&quot;act-four--down-the-corridor&quot;&gt;Act Four — Down the Corridor&lt;/h2&gt;
&lt;p&gt;The Concierge has, by now, memorised the face of every resident on his corridor. Frieda the Frontend, Apartment 10 — of course, of course. He whisks the parcel down the hall and slides it through the slot in her door.&lt;/p&gt;
&lt;p&gt;Frieda is, as ever, at her front desk in a sunny mood. She unwraps the parcel — courier sleeve off, the building-layer wrapping off, the original travel packet off — and unfolds the small inner envelope and finally reads the note inside.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“This customer would like to buy one toaster.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;“Right!” Frieda says, brightly, to nobody. “That’s Brutus’s department.”&lt;/p&gt;
&lt;p&gt;She produces a clean piece of paper and writes &lt;em&gt;her own&lt;/em&gt; note: &lt;em&gt;“a customer would like to buy one toaster — please process.”&lt;/em&gt; She wraps it in her own small envelope, addresses the envelope to Apartment 11 next door, wraps that in her own little courier sleeve, and slides the whole thing back out under her door for the Concierge.&lt;/p&gt;
&lt;p&gt;This is important. Frieda did not forward the original parcel. She wrote a &lt;em&gt;new&lt;/em&gt; parcel, with a new address, asking Brutus for the same thing on her behalf. The original parcel sits on her desk like a receipt. She’ll need it again in a minute.&lt;/p&gt;
&lt;p&gt;The Concierge takes Frieda’s new parcel — three short steps down the hall — and slides it under Brutus’s door.&lt;/p&gt;
&lt;p&gt;Brutus, in Apartment 11, does not look up from his ledger. He hears the parcel arrive. He sighs. He unwraps it. He reads. He grumbles. &lt;em&gt;“Toaster. Customer. Fine.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;He turns to his great accounting book. He opens it. He debits one toaster from the inventory. He writes a note next to Jack’s name. He stamps the whole thing &lt;em&gt;CONFIRMED&lt;/em&gt; in red ink. Then he writes a tiny reply — &lt;em&gt;“order received, will ship, ID #42”&lt;/em&gt; — wraps it in a small envelope addressed to Frieda, and slides it back into the hall.&lt;/p&gt;
&lt;p&gt;Concierge. Three steps. Frieda’s slot.&lt;/p&gt;
&lt;p&gt;Frieda reads Brutus’s reply, smiles, and now turns back to the parcel she had set aside. She picks up her pen and writes a reply of her own, addressed back to the original sender — &lt;em&gt;Jack&lt;/em&gt; — saying &lt;em&gt;“order confirmed, your toaster is on its way, thank you for shopping with us.”&lt;/em&gt; She wraps it in the same nesting style she received: envelope inside packet inside courier sleeve. She slides it under her door.&lt;/p&gt;
&lt;h2 id=&quot;act-five--home&quot;&gt;Act Five — Home&lt;/h2&gt;
&lt;p&gt;The reply travels in reverse, exactly the way the request came. Concierge to Mama Kernel. Mama Kernel consults her directory: &lt;em&gt;the sender lives way out beyond the building’s walls&lt;/em&gt;, so she’ll need to send it out via the Front Gate.&lt;/p&gt;
&lt;p&gt;But before she can wrap it for outbound travel, the Post-Clerk appears at her elbow. He flips open his ledger to the page about parcel #8472.&lt;/p&gt;
&lt;p&gt;“Don’t forget,” he says. “When this came in, my colleague rewrote the address from the building’s name to Apartment 10. We need to reverse that now. Frieda’s apartment number should not appear on this parcel as it leaves — to the outside world, the reply came &lt;em&gt;from the building&lt;/em&gt;, not from any one apartment.”&lt;/p&gt;
&lt;p&gt;He pencils in the correction. He closes the ledger.&lt;/p&gt;
&lt;p&gt;Mama Kernel wraps the now-correctly-addressed packet in a fresh outer satchel and hands it to the Front Gate, who passes it to the first relay runner waiting outside. The Sea of Couriers reverses the relay across the continent, satchel by satchel, until at last a single runner sprints up Jack’s driveway and slips the parcel under his door.&lt;/p&gt;
&lt;p&gt;Browser-Bot receives it, unwraps it ceremonially, smooths out the reply note, and reads.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“Order confirmed. Your toaster is on its way. Thank you for shopping with us.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In Jack’s browser, the Buy button quietly turns into a green checkmark and changes its name to &lt;strong&gt;✓ Ordered&lt;/strong&gt;. Somewhere, a cat looks up from a sunny patch on the rug. Jack smiles.&lt;/p&gt;
&lt;p&gt;He thinks: &lt;em&gt;that was so easy&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-decoder-ring&quot;&gt;The Decoder Ring&lt;/h2&gt;
&lt;p&gt;It was not, in fact, easy.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The full journey of one web request from Jack&amp;#39;s click to the response on his screen: packet nesting at the browser, routers rebuilding the Ethernet frame at every hop, the Linux host pipeline, the conntrack snapshot that lets replies undo the NAT, the frontend-to-backend hop that stays entirely on the podman0 bridge, and the symmetrical reply path back to the client.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1491&quot; height=&quot;1055&quot; src=&quot;/_astro/how_a_web_request_travels.BRiMf_cn_Z1n5NSi.webp&quot; srcset=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here is what each character secretly was. Read it the way you’d read the credits at the end of a play and realise you’ve been watching something more elaborate than you thought.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jack and his Browser-Bot.&lt;/strong&gt; A user at a computer, and the web browser running on that computer. The browser is the program that translates human intent — “I clicked Buy” — into the actual sequence of messages a server needs to receive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The note Browser-Bot wrote, and the nesting wrappers.&lt;/strong&gt; This is the heart of the whole story, so I’ll spend a moment on it.&lt;/p&gt;
&lt;p&gt;Every message that crosses the internet is built like a nesting doll, with each wrapper meant for a different audience. The innermost note is the actual content — in our case, “buy one toaster.” That’s called an &lt;strong&gt;HTTP request&lt;/strong&gt;, the language web browsers and web servers use to talk to each other.&lt;/p&gt;
&lt;p&gt;Around that, Browser-Bot wraps a &lt;strong&gt;TCP segment&lt;/strong&gt; — a wrapper that adds reliability features (sequence numbers, retransmission, confirmation receipts) so that if any part of the journey loses the parcel, it can be detected and resent. TCP is what makes the internet &lt;em&gt;reliable&lt;/em&gt;; without it, every web page would be a coin flip.&lt;/p&gt;
&lt;p&gt;Around &lt;em&gt;that&lt;/em&gt;, an &lt;strong&gt;IP packet&lt;/strong&gt; — the layer that carries the actual end-to-end address. The IP packet says &lt;em&gt;this is for the shop’s public address&lt;/em&gt;. The IP packet is the one wrapper that, like in the story, mostly never gets opened or rewritten as it crosses the continent. It travels end to end.&lt;/p&gt;
&lt;p&gt;Around &lt;em&gt;that&lt;/em&gt;, an &lt;strong&gt;Ethernet frame&lt;/strong&gt; — the outermost satchel. The frame only knows about the &lt;em&gt;next single hop&lt;/em&gt;. It’s addressed to the next runner, not to the final destination. At every hop along the way, the frame gets torn off and a new one written for the next hop.&lt;/p&gt;
&lt;p&gt;That nesting — letter inside TCP inside IP inside Ethernet — is the famous “OSI layered model” most of us were taught in school as seven boxes to memorise. It is, in practice, just this: a parcel inside a parcel inside a parcel, each for a different audience along the route.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Sea of Couriers.&lt;/strong&gt; Routers across the public internet. Each router only knows the next hop; together they form a relay across continents. The fact that the inner IP packet never gets rewritten — only the outer Ethernet frame, fresh at every hop — is what makes the internet actually work. The IP packet survives end to end while the wrappers around it are disposable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Front Gate.&lt;/strong&gt; The host’s network card — the &lt;strong&gt;NIC&lt;/strong&gt; — facing the outside world. On a Linux box this interface is usually called &lt;code&gt;eth0&lt;/code&gt;. Its job is exactly as described: accept frames addressed to us, drop the rest, hand the accepted ones up to the kernel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mama Kernel.&lt;/strong&gt; The Linux kernel itself. She owns every interface in the building, every routing decision, every address rewrite, every connection in flight. The directory she consults is the &lt;strong&gt;kernel routing table&lt;/strong&gt;; the rulebook beside it is &lt;strong&gt;iptables / netfilter&lt;/strong&gt;; the great ledger of every connection she’s tracking is the &lt;strong&gt;conntrack table&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-Clerk and Post-Clerk.&lt;/strong&gt; These are the iptables hooks called &lt;strong&gt;PREROUTING&lt;/strong&gt; and &lt;strong&gt;POSTROUTING&lt;/strong&gt;. The rewriting they do is &lt;strong&gt;NAT&lt;/strong&gt; — Network Address Translation. The specific kind of rewriting in our story (an incoming connection getting redirected to an internal apartment number) is called &lt;strong&gt;DNAT&lt;/strong&gt; (Destination NAT). The reason Post-Clerk has to reverse the rewrite on the way out is so the outside world only ever sees the building’s public address, never the internal apartment numbers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Hallway Concierge.&lt;/strong&gt; A &lt;strong&gt;virtual bridge&lt;/strong&gt; inside the kernel. On a Linux box running containers, it’s commonly named &lt;code&gt;docker0&lt;/code&gt; or &lt;code&gt;podman0&lt;/code&gt;. It is, behaviourally, &lt;em&gt;exactly&lt;/em&gt; the kind of switch we explored in &lt;a href=&quot;../networking-journey-portainer-and-switches/&quot;&gt;post 1&lt;/a&gt; — a thing that delivers messages between residents of one corridor by name, by lookup or by flooding, and does nothing else. The fact that it’s software and not a metal box in a rack is a detail; the algorithm is identical.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frieda the Frontend and Brutus the Backend.&lt;/strong&gt; Two &lt;strong&gt;containers&lt;/strong&gt;, each living in its own isolated apartment. Frieda might run, say, an nginx server that handles incoming web traffic. Brutus might run a database or an order-processing API. They’re plugged into the same internal corridor (the bridge) but isolated from each other — and from the rest of the building — in a way we still have to talk about.&lt;/p&gt;
&lt;p&gt;The moment Frieda decided to “write a new parcel to Brutus” instead of “forwarding the original” is exactly the moment a real reverse proxy makes a fresh request to an upstream service. It is two &lt;em&gt;separate&lt;/em&gt; conversations stitched together, not one conversation passed through. That distinction matters more than it sounds; we’ll come back to it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apartment 10 and 11, the corridor, the building.&lt;/strong&gt; These are the IPs you’ve been seeing all over post 1 and this one. Frieda’s apartment number is &lt;code&gt;10.88.0.10&lt;/code&gt;. Brutus’s is &lt;code&gt;10.88.0.11&lt;/code&gt;. The corridor is the &lt;code&gt;10.88.0.0/16&lt;/code&gt; subnet. The host — Mama Kernel’s own seat at the corridor’s table — is &lt;code&gt;10.88.0.1&lt;/code&gt;. The building’s public address is whatever IP the outside world sees the host as; that one varies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The conversation between Frieda and Brutus that the outside world never saw.&lt;/strong&gt; This is the inter-container traffic on the bridge. Notice it crossed &lt;em&gt;no&lt;/em&gt; NAT, &lt;em&gt;no&lt;/em&gt; routing decision worth speaking of, &lt;em&gt;no&lt;/em&gt; outside interface. Two apartments, one corridor, Concierge passing parcels. Cheap, fast, invisible from outside. This is why container-to-container traffic on a bridge is so quick — the kernel basically never has to leave the lower layers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The two ledgers being kept in lockstep.&lt;/strong&gt; That’s connection tracking — &lt;code&gt;conntrack&lt;/code&gt; — the kernel feature that remembers every active connection and what rewrites were applied to it, so reply traffic can be reversed perfectly even though the rewriting was applied asymmetrically. Without this, no NAT scheme on Earth would work. With it, the entire modern internet (where every home router does this, all day, for every device on your home network) functions invisibly.&lt;/p&gt;
&lt;h2 id=&quot;the-aha&quot;&gt;The Aha&lt;/h2&gt;
&lt;p&gt;When my friend at dinner heard “the internet,” she pictured, I think, a kind of pneumatic tube. You put a thing in one end, it comes out the other. Magic in the middle.&lt;/p&gt;
&lt;p&gt;The middle is not magic. The middle is a relay of small, dumb, identical actors, each doing one tiny job per parcel, none of them aware of the others, none of them aware of the contents. The “intelligence” lives at the two ends — Browser-Bot wrapping the parcel with the right addresses on each layer, and the receiving server unwrapping them and replying. The middle is just a sequence of helpful strangers passing notes.&lt;/p&gt;
&lt;p&gt;Inside the office building, the relay continues. Mama Kernel is more sophisticated than any one courier — she can rewrite addresses, route between corridors, keep ledgers — but she is still, fundamentally, a series of small mechanisms in sequence. Receive at the door. Strip the outer wrapper. Look at the address. Maybe rewrite it. Look up where it goes. Wrap it again. Send it out the right inner door. That’s a pipeline, not a wizard. You can hold the whole thing in your head once you stop expecting it to be one big magic step.&lt;/p&gt;
&lt;p&gt;This was the second click for me, after the switch one. The switch unlocked &lt;em&gt;what happens at one moment in time, in one place&lt;/em&gt;. The toaster story unlocked &lt;em&gt;what happens across an entire journey&lt;/em&gt;. They go together.&lt;/p&gt;
&lt;h2 id=&quot;what-i-still-dont-understand&quot;&gt;What I Still Don’t Understand&lt;/h2&gt;
&lt;p&gt;There’s a moment in the story I quietly waved past.&lt;/p&gt;
&lt;p&gt;When the parcel arrived at Frieda’s apartment, I said her “local kernel” peeled the wrappers, her own door slot received the courier sleeve, her own desk held the original packet. Frieda has, apparently, her own little internal world: her own front door, her own loopback to herself, her own apartment number, her own desk, her own everything.&lt;/p&gt;
&lt;p&gt;But Frieda isn’t a separate computer. Frieda is just a process running on the same host as Mama Kernel. There is only one kernel in this building. There is only one set of hardware.&lt;/p&gt;
&lt;p&gt;So how does Frieda’s apartment &lt;em&gt;exist&lt;/em&gt;? How does it have its own internal addresses, its own loopback, its own private experience of the corridor that doesn’t trample on Brutus’s right next door? How does the same kernel run multiple parallel &lt;em&gt;network worlds&lt;/em&gt; side by side, each believing it’s alone?&lt;/p&gt;
&lt;p&gt;The answer is the feature I waved at in post 1 without explaining. It’s called a &lt;strong&gt;network namespace&lt;/strong&gt;, and once it clicks, the entire reason containers feel like “their own little computers” snaps into focus. It’s also the foundation under VPNs, under sandboxing, under a lot of zero-trust networking — and under the bug I’m slowly explaining across this whole series.&lt;/p&gt;
&lt;p&gt;I’ll get there. I promise.&lt;/p&gt;
&lt;p&gt;See you next post — whatever rabbit hole I fall into first.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;(Written by Human, improved using AI where applicable.)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate></item><item><title>What Actually Is ASGI? A Journey from Confusion to Clarity</title><link>https://shanechang.com/p/understanding-asgi-from-confusion-to-clarity/</link><guid isPermaLink="true">https://shanechang.com/p/understanding-asgi-from-confusion-to-clarity/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.D4v3duLW_217oEx.webp&quot; alt=&quot;Featured image of post What Actually Is ASGI? A Journey from Confusion to Clarity&quot; /&gt;&lt;h2 id=&quot;the-mystery-of-the-invisible-scope&quot;&gt;The Mystery of the Invisible Scope&lt;/h2&gt;
&lt;p&gt;I was building a feature that needed real-time updates. Nothing fancy—just wanted to push some data to clients as things changed on the server. Naturally, I reached for FastAPI and started reading through the documentation. That’s when I first encountered it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scope&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The docs mentioned it everywhere. Tutorial code referenced it. Stack Overflow answers assumed I knew what it was. But here’s the thing that drove me crazy: &lt;strong&gt;I couldn’t find it in any of my actual code&lt;/strong&gt;. It was like everyone was talking about this mysterious variable that just… existed somewhere. Nowhere in my route handlers, nowhere in my WebSocket endpoints. Where was this magical &lt;code&gt;scope&lt;/code&gt; object?&lt;/p&gt;
&lt;p&gt;I felt like I’d walked into the middle of a conversation where everyone understood the context except me.&lt;/p&gt;
&lt;p&gt;Then I started seeing &lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; mentioned too. Same problem—referenced everywhere, visible nowhere in the framework code I was writing. AI assistants kept explaining things in terms of these three parameters, but I couldn’t connect the abstract explanations to the concrete code I was looking at in Litestar and Advanced Alchemy documentation.&lt;/p&gt;
&lt;p&gt;That’s when I realized: I’d been learning frameworks without understanding what they were built on. I needed to go deeper—to the bare metal of how Python async web servers actually work. Not the FastAPI way or the Django Channels way, but the fundamental protocol underneath.&lt;/p&gt;
&lt;p&gt;This is the story of that journey. If you’ve ever felt confused about ASGI, WebSockets, or why async Python web stuff feels so different from traditional Flask/Django, this is for you. By the end, you’ll understand not just what ASGI is, but &lt;em&gt;why&lt;/em&gt; it exists and how it creates a unified model for everything from simple HTTP requests to long-lived WebSocket connections.&lt;/p&gt;
&lt;h2 id=&quot;the-first-aha-moment-asgi-is-just-a-contract&quot;&gt;The First Aha Moment: ASGI Is Just a Contract&lt;/h2&gt;
&lt;p&gt;Here’s what finally clicked for me: &lt;strong&gt;ASGI isn’t a framework or a library. It’s a specification—a contract for how web servers talk to Python applications.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Think of it like this: back in the day, everyone agreed on how electrical outlets should work. Any appliance manufacturer could build a device knowing it would fit the standard outlet. ASGI is the same idea, but for async Python web servers and applications.&lt;/p&gt;
&lt;p&gt;The entire contract boils down to three parameters:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;async def app(scope, receive, send):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # Your application logic here&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. That’s the whole interface. Every ASGI application—whether it’s FastAPI, Starlette, Django Channels, or something you build from scratch—is fundamentally just a callable that accepts these three parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/strong&gt;: A dictionary containing metadata about the connection. Think of it as the “context” or “session info”—what kind of connection is this? Where’s it coming from? What’s being requested?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;receive&lt;/code&gt;&lt;/strong&gt;: An async function you call to get messages from the client. Like checking your mailbox for incoming letters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;send&lt;/code&gt;&lt;/strong&gt;: An async function you call to send messages to the client. Like putting outgoing letters in the mailbox.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The beautiful part? This same simple contract handles everything—regular HTTP requests, streaming Server-Sent Events, bidirectional WebSockets, even application startup and shutdown. The &lt;code&gt;scope&lt;/code&gt; just tells you &lt;em&gt;which kind of communication pattern&lt;/em&gt; you’re dealing with.&lt;/p&gt;
&lt;p&gt;But here’s what confused me at first: what exactly counts as a “session”? Is it one request? A conversation? The lifetime of the server?&lt;/p&gt;
&lt;p&gt;The answer is: &lt;strong&gt;it depends on the scope type&lt;/strong&gt;. And that’s where things get interesting.&lt;/p&gt;
&lt;h2 id=&quot;http-the-simple-transaction&quot;&gt;HTTP: The Simple Transaction&lt;/h2&gt;
&lt;p&gt;Let me start with the familiar: plain old HTTP requests. This is probably what you already understand intuitively, even if you didn’t know ASGI was involved.&lt;/p&gt;
&lt;p&gt;When a client makes an HTTP request to your server, ASGI creates a scope that looks something like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;method&quot;: &quot;GET&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/api/users/123&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;format=json&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54321),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;type: &quot;http&quot;&lt;/code&gt; is the key—it tells your application “this is a simple HTTP request-response transaction.”&lt;/p&gt;
&lt;p&gt;Here’s the flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client sends request → Server creates scope and calls your app&lt;/li&gt;
&lt;li&gt;Your app calls &lt;code&gt;receive()&lt;/code&gt; to get the request body (maybe in chunks)&lt;/li&gt;
&lt;li&gt;Your app calls &lt;code&gt;send()&lt;/code&gt; with response.start (status code, headers)&lt;/li&gt;
&lt;li&gt;Your app calls &lt;code&gt;send()&lt;/code&gt; again with response.body (the actual content)&lt;/li&gt;
&lt;li&gt;Scope ends. Connection done.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Think of it like a vending machine: you insert money (request), press a button (the route), and get a snack back (response). Transaction complete. The machine doesn’t remember you existed.&lt;/p&gt;
&lt;p&gt;That “doesn’t remember” part is crucial—HTTP in ASGI is &lt;strong&gt;stateless&lt;/strong&gt;. Each request is independent. The scope lives for maybe 100 milliseconds, just long enough to handle one request-response cycle.&lt;/p&gt;
&lt;p&gt;Example messages you’d send:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# First, send the response headers and status&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;application/json&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Then send the actual response body&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&apos;{&quot;user&quot;: &quot;Shane&quot;, &quot;id&quot;: 123}&apos;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, stateless, short-lived. This is the foundation—the easiest pattern to understand.&lt;/p&gt;
&lt;h2 id=&quot;server-sent-events-the-one-way-stream&quot;&gt;Server-Sent Events: The One-Way Stream&lt;/h2&gt;
&lt;p&gt;Now, what if your vending machine needed to keep telling you about new snacks as they were restocked? You’d want to stay connected and receive updates, but you wouldn’t be sending anything back except “yes, keep the updates coming.”&lt;/p&gt;
&lt;p&gt;That’s Server-Sent Events (SSE).&lt;/p&gt;
&lt;p&gt;Here’s what confused me initially: SSE uses an HTTP scope, but it doesn’t follow the typical request-response pattern. Instead:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client makes an HTTP request&lt;/li&gt;
&lt;li&gt;Server responds with headers (including &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection stays open&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Server keeps sending chunks of data whenever it wants&lt;/li&gt;
&lt;li&gt;Eventually, either side closes the connection&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The scope is still &lt;code&gt;type: &quot;http&quot;&lt;/code&gt;, but the interaction is different. It’s like calling a restaurant for their daily specials, and instead of hanging up after they tell you today’s menu, they keep you on the line and tell you every time a new special is added.&lt;/p&gt;
&lt;p&gt;Example of what the server might send:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;text/event-stream&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Then keep sending updates...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 42}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,  # Signal that more data is coming&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# ... later ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 43}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice &lt;code&gt;more_body: True&lt;/code&gt;? That’s the key. It tells ASGI “don’t close the connection, I’ve got more to send.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSE is still fundamentally HTTP&lt;/strong&gt;—it’s just long-lived HTTP. The client doesn’t send data back through this connection (it’s one-way), but the connection can stay open for minutes or even hours.&lt;/p&gt;
&lt;p&gt;When to use SSE:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Live dashboards showing real-time metrics&lt;/li&gt;
&lt;li&gt;Notification feeds&lt;/li&gt;
&lt;li&gt;Stock tickers&lt;/li&gt;
&lt;li&gt;Progress updates for long-running tasks&lt;/li&gt;
&lt;li&gt;Any scenario where the server pushes updates but doesn’t need responses&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s simpler than WebSockets because it’s unidirectional, but more powerful than regular HTTP because it’s persistent.&lt;/p&gt;
&lt;h2 id=&quot;websocket-the-full-conversation&quot;&gt;WebSocket: The Full Conversation&lt;/h2&gt;
&lt;p&gt;Then I hit WebSockets. And this is where my understanding of ASGI really got tested.&lt;/p&gt;
&lt;p&gt;WebSockets are fundamentally different from both HTTP and SSE because they’re &lt;strong&gt;bidirectional and stateful&lt;/strong&gt;. This isn’t a transaction or a one-way stream—it’s a ongoing conversation where both sides can speak whenever they want.&lt;/p&gt;
&lt;p&gt;If HTTP is like sending letters and SSE is like listening to a radio broadcast, &lt;strong&gt;WebSocket is like a phone call&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The scope for WebSocket looks different:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;websocket&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/ws/chat/room-42&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;user=shane&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54322),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice: &lt;code&gt;type: &quot;websocket&quot;&lt;/code&gt;. This signals a completely different kind of interaction.&lt;/p&gt;
&lt;p&gt;WebSocket messages include:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Incoming (from client to your app):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.connect&quot;}       # Client wants to start a session&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.receive&quot;, &quot;text&quot;: &quot;Hello!&quot;}  # Client sent a message&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.disconnect&quot;}    # Client ended the session&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Outgoing (from your app to client):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.accept&quot;}        # You approve the connection&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.send&quot;, &quot;text&quot;: &quot;Welcome!&quot;}   # You send a message&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.close&quot;}         # You end the session&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See the difference? With HTTP, you receive a request and send a response. With WebSocket, you’re dealing with a &lt;strong&gt;state machine&lt;/strong&gt;: connect → accept → ongoing send/receive loop → close.&lt;/p&gt;
&lt;p&gt;The scope lives for the entire WebSocket session—could be seconds, could be hours. You might exchange hundreds of messages within a single scope. This is fundamentally stateful—the server remembers this connection exists and can push data to it anytime.&lt;/p&gt;
&lt;p&gt;When to use WebSocket:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time chat applications&lt;/li&gt;
&lt;li&gt;Collaborative editing (like Google Docs)&lt;/li&gt;
&lt;li&gt;Multiplayer games&lt;/li&gt;
&lt;li&gt;Live video/audio streaming control&lt;/li&gt;
&lt;li&gt;Any scenario requiring true bidirectional real-time communication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But here’s what really confused me when I first learned this…&lt;/p&gt;
&lt;h2 id=&quot;the-mystery-of-the-websocket-handshake&quot;&gt;The Mystery of the WebSocket Handshake&lt;/h2&gt;
&lt;p&gt;I kept seeing references to “WebSocket upgrade” and “HTTP to WebSocket upgrade handshake.” And I had this burning question: &lt;strong&gt;if WebSocket is different from HTTP, how does it even start?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here’s the part that blew my mind: &lt;strong&gt;WebSockets always begin as HTTP requests&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Wait, what?&lt;/p&gt;
&lt;p&gt;Let me explain. WebSockets were designed to work with existing web infrastructure—the same ports, the same proxies, the same SSL/TLS setup. So instead of inventing an entirely new protocol from scratch, they built it on top of HTTP.&lt;/p&gt;
&lt;p&gt;Here’s what actually happens when a browser wants to open a WebSocket connection:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Client sends a special HTTP request&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;GET /ws/chat HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Host: example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Key: X3JJHMbDL1EzLkh9GBhXDw==&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Version: 13&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a regular HTTP GET request, but with special headers saying “hey, I’d like to upgrade this connection to WebSocket.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Server responds with HTTP 101&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HTTP/1.1 101 Switching Protocols&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Accept: &amp;#x3C;computed response&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;101 Switching Protocols&lt;/code&gt; status is the magic handshake. It means: “Okay, we’re no longer speaking HTTP. From this moment forward, this TCP connection will use WebSocket protocol instead.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Protocol switch happens&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The underlying TCP connection stays open, but HTTP is done. Now both sides start speaking WebSocket framing protocol—a completely different language.&lt;/p&gt;
&lt;p&gt;Think of it like this: You call a restaurant (HTTP), ask to be transferred to a specific table (upgrade request), the host says “transferring you now” (101 response), and suddenly you’re having a direct conversation with someone at that table (WebSocket). Same phone line, different conversation protocol.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-at-the-asgi-layer&quot;&gt;What Happens at the ASGI Layer?&lt;/h2&gt;
&lt;p&gt;Here’s what confused me the most: &lt;strong&gt;where does ASGI fit into this handshake?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The answer: ASGI comes in &lt;em&gt;after&lt;/em&gt; the handshake is complete.&lt;/p&gt;
&lt;p&gt;Let me break down what happens at different layers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server Level (Uvicorn/Hypercorn—automatic, not your code):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Receives the HTTP upgrade request&lt;/li&gt;
&lt;li&gt;Validates WebSocket headers&lt;/li&gt;
&lt;li&gt;Sends back the 101 Switching Protocols response&lt;/li&gt;
&lt;li&gt;Switches the TCP connection to WebSocket framing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ASGI Level (your application code):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server creates a WebSocket scope&lt;/li&gt;
&lt;li&gt;Server sends you a &lt;code&gt;websocket.connect&lt;/code&gt; message&lt;/li&gt;
&lt;li&gt;Now YOU decide: accept or reject?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This two-layer decision-making finally made sense when I understood it this way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Protocol layer (server):&lt;/strong&gt; “Is this a valid WebSocket upgrade request?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application layer (your code):&lt;/strong&gt; “Do I &lt;em&gt;want&lt;/em&gt; to allow this specific connection?” (authentication, authorization, rate limiting, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The handshake is already done by the time your application sees &lt;code&gt;websocket.connect&lt;/code&gt;. The client is already “connected” at the protocol level. Your &lt;code&gt;websocket.accept&lt;/code&gt; isn’t completing the handshake—it’s your application’s business logic saying “yes, I approve this session.”&lt;/p&gt;
&lt;p&gt;Example flow in your ASGI app:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# You receive this from the server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;message = await receive()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# message = {&quot;type&quot;: &quot;websocket.connect&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Now you decide - maybe check authentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if user_is_authenticated:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.accept&quot;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # Now the bidirectional message loop can begin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.close&quot;, &quot;code&quot;: 1008})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # Reject the connection - unauthorized&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This distinction between server-level (protocol) and application-level (business logic) was my biggest “aha!” moment. The framework documentation suddenly made sense—FastAPI’s WebSocket endpoints, Starlette’s connection handling, all of it was operating at the application layer, trusting the server (Uvicorn) to handle the protocol layer.&lt;/p&gt;
&lt;p&gt;Either side can close the connection at any time. There’s no “client is in control” or “server is in control”—it’s a true peer-to-peer conversation (well, as peer-to-peer as client-server can be). When either side sends a close message, the WebSocket session ends and the scope completes.&lt;/p&gt;
&lt;h2 id=&quot;lifespan-the-orthogonal-dimension&quot;&gt;Lifespan: The Orthogonal Dimension&lt;/h2&gt;
&lt;p&gt;Just when I thought I understood the pattern—HTTP, SSE, and WebSocket representing different communication patterns—I encountered a fourth scope type that didn’t fit the model at all.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lifespan&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here’s what tripped me up: I initially thought “lifespan” meant “the lifespan of a connection.” Like, the duration from when a WebSocket connects until it disconnects. That made intuitive sense!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I was completely wrong.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lifespan has nothing to do with individual requests or connections. It’s about &lt;strong&gt;the application itself&lt;/strong&gt;—the entire server process from startup to shutdown.&lt;/p&gt;
&lt;p&gt;Think of it this way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP scope: exists for one request (~100ms)&lt;/li&gt;
&lt;li&gt;SSE scope: exists for one streaming session (minutes to hours)&lt;/li&gt;
&lt;li&gt;WebSocket scope: exists for one bidirectional conversation (seconds to hours)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifespan scope: exists for the entire application (days to months)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When your server starts up, before it handles any HTTP requests or WebSocket connections, it creates a lifespan scope and sends you startup messages. When the server is shutting down (gracefully), it sends you shutdown messages.&lt;/p&gt;
&lt;p&gt;The scope looks like:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;lifespan&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The messages you receive:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.startup&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.shutdown&quot;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What would you use this for? Things that need to happen once per application, not per request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Opening database connection pools&lt;/li&gt;
&lt;li&gt;Loading machine learning models into memory&lt;/li&gt;
&lt;li&gt;Starting background task schedulers&lt;/li&gt;
&lt;li&gt;Warming caches&lt;/li&gt;
&lt;li&gt;Setting up monitoring/metrics collectors&lt;/li&gt;
&lt;li&gt;Graceful cleanup on shutdown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Analogy time: If HTTP/SSE/WebSocket are like serving individual customers in a restaurant, lifespan is like opening the restaurant in the morning (turning on ovens, prepping ingredients) and closing it at night (cleaning up, shutting down equipment).&lt;/p&gt;
&lt;p&gt;You don’t want to load a 2GB ML model on every HTTP request—you load it once during &lt;code&gt;lifespan.startup&lt;/code&gt; and reuse it for all requests. You don’t want to abruptly kill database connections—you close them gracefully during &lt;code&gt;lifespan.shutdown&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In FastAPI, this is what those &lt;code&gt;@app.on_event(&quot;startup&quot;)&lt;/code&gt; and &lt;code&gt;@app.on_event(&quot;shutdown&quot;)&lt;/code&gt; decorators are doing—they’re handling lifespan messages for you.&lt;/p&gt;
&lt;p&gt;Lifespan is orthogonal to the other three scope types. It’s not about communication patterns between client and server—it’s about the lifecycle of the server process itself.&lt;/p&gt;
&lt;h2 id=&quot;the-unified-mental-model&quot;&gt;The Unified Mental Model&lt;/h2&gt;
&lt;p&gt;After all this confusion, trial, error, and eventual clarity, here’s the mental model that finally made ASGI click for me:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASGI is a universal interface for describing “communication sessions” between servers and applications, where a session can be:&lt;/strong&gt;&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scope Type&lt;/th&gt;&lt;th&gt;Duration&lt;/th&gt;&lt;th&gt;Direction&lt;/th&gt;&lt;th&gt;Use Case&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Milliseconds&lt;/td&gt;&lt;td&gt;Request → Response&lt;/td&gt;&lt;td&gt;Simple API calls, page loads, form submissions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Minutes to hours&lt;/td&gt;&lt;td&gt;Server → Client (one-way)&lt;/td&gt;&lt;td&gt;Live feeds, dashboards, notifications&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Seconds to hours&lt;/td&gt;&lt;td&gt;Bidirectional&lt;/td&gt;&lt;td&gt;Chat, collaboration, games, real-time updates&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Lifespan&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Application lifetime&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;td&gt;Startup/shutdown tasks, resource management&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;All four use the exact same interface: &lt;code&gt;async def app(scope, receive, send)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;scope&lt;/code&gt; dictionary tells you what kind of session you’re dealing with. The &lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; functions let you interact with that session. The same contract, different behaviors.&lt;/p&gt;
&lt;p&gt;What makes this elegant is that &lt;strong&gt;middleware can work across all types&lt;/strong&gt;. Want to add authentication? Write middleware that checks the scope type and handles HTTP, WebSocket, and SSE appropriately. Want to add logging? Same deal—one interface, universal application.&lt;/p&gt;
&lt;p&gt;This is why frameworks like FastAPI feel so natural once you understand ASGI. Under the hood, every route handler, every WebSocket endpoint, every startup event—they’re all just ASGI applications conforming to this same simple contract.&lt;/p&gt;
&lt;p&gt;The mystery of the invisible &lt;code&gt;scope&lt;/code&gt; finally made sense. It’s there in every ASGI application—frameworks just abstract it away so you don’t have to think about it for simple cases. But when you need to go deeper, when you need to understand &lt;em&gt;why&lt;/em&gt; WebSockets work differently from HTTP or &lt;em&gt;where&lt;/em&gt; to put your database initialization, understanding the bare ASGI layer gives you that clarity.&lt;/p&gt;
&lt;h2 id=&quot;from-confusion-to-clarity&quot;&gt;From Confusion to Clarity&lt;/h2&gt;
&lt;p&gt;When I started this journey, &lt;code&gt;scope&lt;/code&gt; was an invisible, mysterious thing that documentation assumed I understood. Now I see it for what it is: a simple dictionary describing a communication pattern.&lt;/p&gt;
&lt;p&gt;The beautiful part is how this knowledge transfers. Now when I read FastAPI docs and see WebSocket examples, I understand what’s happening beneath the decorator syntax. When I see Starlette’s startup events, I know they’re handling lifespan messages. When I troubleshoot why a connection isn’t staying open, I can reason about whether it’s an HTTP scope that ended naturally or a WebSocket that closed unexpectedly.&lt;/p&gt;
&lt;p&gt;Going to the bare metal—understanding the actual protocol instead of just the framework—transformed my confusion into confidence.&lt;/p&gt;
&lt;p&gt;If you’re building real-time features, async APIs, or just trying to understand modern Python web development, I hope this journey helps demystify ASGI for you the way it did for me. Next time you see that mysterious &lt;code&gt;scope&lt;/code&gt; parameter in the documentation, you’ll know exactly what it means.&lt;/p&gt;
&lt;p&gt;And maybe, just maybe, you’ll find yourself diving even deeper—implementing a minimal ASGI app from scratch, writing custom middleware, or truly understanding what your framework is doing behind the scenes.&lt;/p&gt;
&lt;p&gt;The rabbit hole goes deeper if you want it to. But at least now you know where the entrance is.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;(Written by Human, improved using AI where applicable.)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate></item></channel></rss>