<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HappyBug</title>
    <description>The latest articles on DEV Community by HappyBug (@happybug).</description>
    <link>https://dev.to/happybug</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2882753%2Fc03df9af-b748-41fd-86be-15300a11cd3e.jpeg</url>
      <title>DEV Community: HappyBug</title>
      <link>https://dev.to/happybug</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/happybug"/>
    <language>en</language>
    <item>
      <title>System Design the Agentic Way — Phase 2: Stateless Horizontal Scaling</title>
      <dc:creator>HappyBug</dc:creator>
      <pubDate>Tue, 14 Jul 2026 11:09:21 +0000</pubDate>
      <link>https://dev.to/happybug/system-design-the-agentic-way-phase-2-stateless-horizontal-scaling-1gi5</link>
      <guid>https://dev.to/happybug/system-design-the-agentic-way-phase-2-stateless-horizontal-scaling-1gi5</guid>
      <description>&lt;h2&gt;
  
  
  Phase 2: Stateless Horizontal Scaling
&lt;/h2&gt;

&lt;p&gt;Phase 1 ended at a wall: a single machine has hard ceilings, and no amount of better code moves them. The obvious answer is "add more servers." Phase 2 is about everything that sentence quietly assumes — and the new failure modes that appear the moment a second machine exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Question&lt;/li&gt;
&lt;li&gt;
The Building Blocks: What "Add a Server" Actually Requires

&lt;ul&gt;
&lt;li&gt;State: The Thing That Breaks First&lt;/li&gt;
&lt;li&gt;The Shared-Nothing Principle&lt;/li&gt;
&lt;li&gt;Sticky Sessions: The Tempting Trap&lt;/li&gt;
&lt;li&gt;Load Balancing Algorithms&lt;/li&gt;
&lt;li&gt;Layer 4 vs Layer 7&lt;/li&gt;
&lt;li&gt;Latency vs Throughput&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Concurrent Requests Don't Run Sequentially&lt;/li&gt;
&lt;li&gt;What the Experiments Showed&lt;/li&gt;
&lt;li&gt;The Fix Reveals the Lesson&lt;/li&gt;
&lt;li&gt;What This Phase Actually Teaches&lt;/li&gt;
&lt;li&gt;Where I'm Still Fuzzy&lt;/li&gt;
&lt;li&gt;Try It Yourself&lt;/li&gt;
&lt;li&gt;Next&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;You have one server and you want three. What has to be true about your application before adding the other two actually helps — and where does the bottleneck go once you do?&lt;/p&gt;

&lt;p&gt;Phase 2 is an argument that "add more servers" is not an infrastructure task. It's an application-architecture constraint that decides whether the extra machines do anything at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Building Blocks: What "Add a Server" Actually Requires
&lt;/h2&gt;

&lt;p&gt;A war story sets the stakes. A food delivery startup ran its entire API on one server. A routine Friday 6pm deploy meant an 8-second restart. During those 8 seconds: 47 lost payment confirmations, 312 connection resets, and — because the process had to reload a 2.3 GB &lt;code&gt;data.json&lt;/code&gt; — a 45-second full recovery. The bill was $23,000 in refunds.&lt;/p&gt;

&lt;p&gt;The root cause isn't the deploy. It's that a single machine can't be restarted without downtime and can't survive its own hardware dying. The fix is to run several identical copies. But copies only work if the servers hold no state — and that word hides most of the difficulty.&lt;/p&gt;

&lt;h3&gt;
  
  
  State: The Thing That Breaks First
&lt;/h3&gt;

&lt;p&gt;Take the Phase 1 server and imagine running three copies of it. Three distinct kinds of state each break in their own way:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type of state&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Why 3 copies break it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistent data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;data.json&lt;/code&gt; on disk&lt;/td&gt;
&lt;td&gt;Each server has its own copy → writes diverge, reads disagree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In-memory counters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;activeConnections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Each server sees only its own slice → no global view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coordination state&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;write queue / locks&lt;/td&gt;
&lt;td&gt;Only valid inside one process → no cross-server ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: anything a server remembers between requests becomes wrong the moment a second server exists, because the next request might land somewhere else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Shared-Nothing Principle
&lt;/h3&gt;

&lt;p&gt;The fix is to externalize &lt;em&gt;all&lt;/em&gt; state to a shared service — a database, a cache like Redis, a metrics store like Prometheus — so that no server owns anything. The server becomes a pure function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request in → (read shared state) → (write shared state) → response out
server remembers NOTHING between requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;shared-nothing&lt;/strong&gt; architecture. Any request can go to any server, because every server is interchangeable. That interchangeability is the entire point — it's what makes servers disposable, and disposable servers are what let you deploy, restart, and lose hardware without an outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sticky Sessions: The Tempting Trap
&lt;/h3&gt;

&lt;p&gt;If a server &lt;em&gt;does&lt;/em&gt; hold per-user state (a login session in memory), you can paper over it by pinning each user to the same server every time — a &lt;strong&gt;sticky session&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stateful  + sticky sessions → user X always routed to Server 2
Stateless + any routing     → user X can be served by any server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap: sticky sessions reintroduce the single-machine failure. When Server 2 dies, every user pinned to it loses their session — logged out, cart emptied — even though two other servers are healthy. Sticky sessions are a temporary crutch. The goal is always full statelessness, where a dying server costs nothing.&lt;/p&gt;

&lt;p&gt;A related temptation is to jump straight to microservices "to reduce blast radius." Scale the monolith horizontally &lt;em&gt;first&lt;/em&gt;: three identical copies, deployed one at a time (a rolling deploy), gives zero-downtime with zero architectural change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monolith, 1 server      → single point of failure
Monolith, 3 servers     → scales, survives restarts, still SIMPLE
Microservices, 20 svcs  → scales, but adds network failures + distributed transactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decompose only when there's a specific reason to (that's Phase 9), not reflexively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Balancing Algorithms
&lt;/h3&gt;

&lt;p&gt;With three interchangeable servers, something has to decide who gets each request. That's the load balancer, and the decision rule matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Round Robin&lt;/td&gt;
&lt;td&gt;A, B, C, A, B, C…&lt;/td&gt;
&lt;td&gt;Equal servers, equal-cost requests&lt;/td&gt;
&lt;td&gt;Ignores how heavy each request is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Least Connections&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Route to server with fewest active connections&lt;/td&gt;
&lt;td&gt;Mixed request durations&lt;/td&gt;
&lt;td&gt;Blind to raw server capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weighted Round Robin&lt;/td&gt;
&lt;td&gt;More traffic to beefier servers&lt;/td&gt;
&lt;td&gt;Heterogeneous hardware&lt;/td&gt;
&lt;td&gt;Static, doesn't adapt to load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random&lt;/td&gt;
&lt;td&gt;Pick any server&lt;/td&gt;
&lt;td&gt;Surprisingly good at scale&lt;/td&gt;
&lt;td&gt;Can cluster at low request counts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a server where &lt;code&gt;/heavy&lt;/code&gt; takes 5 s but &lt;code&gt;/data/:key&lt;/code&gt; takes 5 ms, &lt;strong&gt;Least Connections&lt;/strong&gt; is the right pick. Round Robin would keep handing new requests to a server already stuck on a slow one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Round Robin:  Server A busy on /heavy (5s) → still gets 2 more → queue builds
Least Conn:   Server A shows 3 active     → skip it → route to Server B (0 active) ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 4 vs Layer 7
&lt;/h3&gt;

&lt;p&gt;"Reverse proxy" and "load balancer" are not separate things — any proxy that hides backends can trivially point at several of them. The meaningful distinction is which network layer it operates on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 4 (transport):  routes packets by IP + port. Fast, protocol-blind.
                      Example: AWS Network Load Balancer.
Layer 7 (application): understands HTTP — can route by path, header, cookie.
                      Example: Nginx, HAProxy, Envoy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layer 7 costs a little more per request but can make smart, content-aware routing decisions. This prototype uses Nginx (Layer 7).&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency vs Throughput
&lt;/h3&gt;

&lt;p&gt;Externalizing state has a cost, and naming it correctly matters. Two independent measures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Latency    = how long ONE request takes         (a single car driving A → B)
Throughput = how many requests complete per unit (cars arriving per hour)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding servers (or highway lanes) does &lt;strong&gt;not&lt;/strong&gt; make a single request faster. It lets the system handle more requests at once. Concretely, moving from a local file to an external store:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Phase 1 (local file)&lt;/th&gt;
&lt;th&gt;Phase 2 (external store)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;~0.2 ms&lt;/td&gt;
&lt;td&gt;~5 ms (25× slower)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput&lt;/td&gt;
&lt;td&gt;~5K req/s&lt;/td&gt;
&lt;td&gt;~30K req/s (6× more)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure tolerance&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;td&gt;survives a server dying&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trade is deliberate: accept higher per-request latency in exchange for far higher throughput and the elimination of the single point of failure. As long as latency stays under the ~100 ms human-perception threshold, users don't notice — but they very much notice an outage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concurrent Requests Don't Run Sequentially
&lt;/h2&gt;

&lt;p&gt;A common misread: "if each request now waits 5 ms on the database, 10,000 requests take 50 seconds." That's wrong, and understanding why is the crux of the phase.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;await&lt;/code&gt; yields the event loop. While one request waits on the database, the server starts the next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=0.00 ms  Req 1 arrives → await db.read() → YIELDS
t=0.01 ms  Req 2 arrives → await db.read() → YIELDS
t=0.02 ms  Req 3 arrives → await db.read() → YIELDS
t=2.00 ms  Req 1's response comes back → resume → reply
t=2.01 ms  Req 2's response comes back → resume → reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each request individually experiences 5 ms. They overlap in time. The system does not fall over from per-request latency — it falls over when the &lt;strong&gt;database saturates&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 servers × 10K req/s × 2 DB ops each = 60,000 DB ops/second
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that is the real lesson of horizontal scaling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE:  [Server] ← ceiling was here (Phase 1: ~15K connections)

AFTER:   [Server 1] ─┐
         [Server 2] ─┼─→ [Database] ← ceiling moved HERE
         [Server 3] ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scaling doesn't eliminate bottlenecks. It moves them downstream.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Experiments Showed
&lt;/h2&gt;

&lt;p&gt;The prototype makes this concrete: the Phase 1 server rewritten as an "amnesia server" (all &lt;code&gt;fs&lt;/code&gt; calls replaced with Redis &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;), packaged into one Docker image, run as three containers behind an Nginx load balancer, all sharing one Redis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Nginx (:80) → server1:3000 ─┐
                       server2:3000 ─┼─→ Redis (:6379)  ← shared state
                       server3:3000 ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every response includes &lt;code&gt;served_by: os.hostname()&lt;/code&gt; to prove distribution. Three verifications passed first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load distribution&lt;/strong&gt; — 10 parallel &lt;code&gt;/stats&lt;/code&gt; requests returned 3 different hostnames (a 4-3-3 split).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state&lt;/strong&gt; — a &lt;code&gt;PUT&lt;/code&gt; through one server was immediately readable through the other two. Redis is the single source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server death&lt;/strong&gt; — &lt;code&gt;docker stop server1&lt;/code&gt;, then read: the remaining two servers served all data with zero loss. The server is genuinely disposable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then came breaking it on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 1 — Kill the shared state: a hang is worse than a crash
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker stop&lt;/code&gt; on Redis, then a request. The expectation was an error response. What actually happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl → server → await redis.get() → (never resolves) → request HANGS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No crash, no error — the Redis client silently retries the connection forever, so &lt;code&gt;await redis.get()&lt;/code&gt; never settles. This is the worst failure mode: a crash returns an error instantly, but a hang ties up the connection, and enough hangs exhaust Nginx's connection pool and freeze the entire system. &lt;strong&gt;Always set timeouts on external service calls.&lt;/strong&gt; A fast failure is recoverable; a silent hang cascades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 2 — Restart Redis: did the data survive?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker start&lt;/code&gt; on Redis, then read the old keys. The data was still there. Redis writes an RDB snapshot to disk on a graceful shutdown (&lt;code&gt;SIGTERM&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The caveat matters: a &lt;code&gt;docker kill&lt;/code&gt; (&lt;code&gt;SIGKILL&lt;/code&gt;) or a real crash skips the snapshot, so everything since the last auto-save is lost. Graceful shutdown preserved durability here; an ungraceful one wouldn't have. Real durability guarantees are a Phase 3 topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 3 — Kill the load balancer: the new single point of failure
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker stop&lt;/code&gt; on Nginx, then &lt;code&gt;curl localhost&lt;/code&gt;: connection refused. The servers and Redis were all healthy — reachable directly via &lt;code&gt;docker exec&lt;/code&gt; — but unreachable from outside, because Nginx is the only public entry point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → [Nginx]  → Server 1 → [Redis]
          SPOF       Server 2    SPOF
                     Server 3
                    (redundant)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the servers gained redundancy. Scaling moved the single point of failure (SPOF) from the server to &lt;em&gt;two&lt;/em&gt; new places: the load balancer in front and the shared state behind.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix Reveals the Lesson
&lt;/h2&gt;

&lt;p&gt;Each break has a distinct remedy, and each remedy points at a later phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State divergence across servers&lt;/strong&gt; → externalize every kind of state to a shared service; keep the server a pure function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hang on dependency failure&lt;/strong&gt; → wrap every external call in a timeout (and eventually a circuit breaker — Phase 11).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancer / shared-state SPOF&lt;/strong&gt; → run multiple load balancers behind a cloud LB, and replicate the shared state (Phase 6).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The surface symptoms differ, but the shape is identical to Phase 1: the component that fails determines both the symptom and the fix. What changed is that "the component" is now something &lt;em&gt;downstream&lt;/em&gt; of the server you added — which is exactly what horizontal scaling does to bottlenecks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Phase Actually Teaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Horizontal scaling is an application property, not an infrastructure one.&lt;/strong&gt; Adding servers only helps if the servers are stateless. The hard work is externalizing state (data, counters, coordination); provisioning the machines is the easy part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Shared-nothing makes servers disposable, and disposable is the whole goal.&lt;/strong&gt; When any request can go to any server, you can deploy one at a time, tolerate hardware death, and restart freely — the three things a single machine can never do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Sticky sessions quietly reintroduce single-machine failure.&lt;/strong&gt; Pinning users to servers is a crutch; a dying server takes its pinned users down with it. Full statelessness is the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The load-balancing algorithm must match request-cost variance.&lt;/strong&gt; With uniform requests, Round Robin is fine. With a mix of 5 ms and 5 s requests, Least Connections avoids piling work onto an already-busy server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Scaling moves bottlenecks downstream — it never deletes them.&lt;/strong&gt; Three servers turn one ceiling (connections) into a new one (database throughput at ~60K ops/s) plus two new SPOFs (the LB and the shared state). Know where the ceiling went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. A hang is worse than a crash.&lt;/strong&gt; An unbounded wait on a dead dependency ties up resources and cascades. Every external call needs a timeout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision heuristic:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Need zero-downtime deploys / HA, low complexity   → monolith × N behind a load balancer
Requests vary wildly in cost                      → Least Connections
Uniform requests, equal servers                   → Round Robin
Need path/header/cookie-aware routing             → Layer 7 (Nginx/Envoy)
Need raw packet throughput, protocol-blind        → Layer 4 (cloud NLB)
Independent scaling / team boundaries force it     → microservices (Phase 9), not before
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where I'm Still Fuzzy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timeout values on external calls.&lt;/strong&gt; I know an unbounded wait is the failure — but what's the &lt;em&gt;right&lt;/em&gt; timeout? Too low and you fail healthy-but-slow requests; too high and you hang. It presumably depends on the dependency's p99, but I don't have a principled way to set it yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load balancer redundancy in practice.&lt;/strong&gt; "Run multiple Nginx behind a cloud LB" is the stock answer, but that just pushes the SPOF up to the cloud LB. Where does the regress actually stop — DNS? Anycast? I haven't built the layer above Nginx.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When Least Connections misleads.&lt;/strong&gt; It's blind to server capacity, so a weak server with few connections still looks attractive. In a heterogeneous fleet, when does that go wrong, and is weighted-least-connections the real answer?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The prototype is at &lt;a href="https://github.com/aishwarya-chamanoor/system-design-agenticway" rel="noopener noreferrer"&gt;github.com/aishwarya-chamanoor/system-design-agenticway&lt;/a&gt; in &lt;code&gt;phase-02/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/aishwarya-chamanoor/system-design-agenticway
&lt;span class="nb"&gt;cd &lt;/span&gt;system-design-agenticway/phase-02
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four experiments worth running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hit &lt;code&gt;http://localhost/stats&lt;/code&gt; ten times in parallel — watch &lt;code&gt;served_by&lt;/code&gt; cycle across three hostnames.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT&lt;/code&gt; a value through one request, then &lt;code&gt;GET&lt;/code&gt; it repeatedly — every server returns the same value because Redis owns it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker stop &amp;lt;server-container&amp;gt;&lt;/code&gt; and keep reading — the surviving servers serve everything; the server is disposable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker stop &amp;lt;redis-container&amp;gt;&lt;/code&gt; and send one request — watch it &lt;strong&gt;hang&lt;/strong&gt; instead of erroring. That hang is the lesson.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Phase 3 goes inside the shared store this phase leaned on — asking what "the data survived" really guarantees, and what it costs to make that guarantee real.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of a 12-phase series building distributed systems intuition from first principles. Each phase has a running prototype, failure scenarios, and a gate check before moving on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>System Design the Agentic Way — Phase 1: The Single Machine Ceiling</title>
      <dc:creator>HappyBug</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:34:05 +0000</pubDate>
      <link>https://dev.to/happybug/system-design-the-agentic-way-phase-1-the-single-machine-ceiling-2i8n</link>
      <guid>https://dev.to/happybug/system-design-the-agentic-way-phase-1-the-single-machine-ceiling-2i8n</guid>
      <description>&lt;h1&gt;
  
  
  Phase 1: The Single Machine Ceiling
&lt;/h1&gt;

&lt;p&gt;Before reasoning about distributed systems, you need a concrete model of what a single machine actually runs out of. Phase 1 builds that model from the ground up — not from diagrams, but from pushing a server until it stops accepting connections, and understanding exactly what gave out and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Question&lt;/li&gt;
&lt;li&gt;
The Building Blocks: What Actually Handles a Connection

&lt;ul&gt;
&lt;li&gt;File Descriptors&lt;/li&gt;
&lt;li&gt;TCP Connections: The Handshake&lt;/li&gt;
&lt;li&gt;Keep-Alive: Efficiency With a Hidden Cost&lt;/li&gt;
&lt;li&gt;TIME_WAIT: The Port Cooldown&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The Resource Landscape&lt;/li&gt;
&lt;li&gt;
Concurrency Models: How the Server Handles Multiple Users

&lt;ul&gt;
&lt;li&gt;Thread-per-Connection&lt;/li&gt;
&lt;li&gt;Event Loop&lt;/li&gt;
&lt;li&gt;Thread Pool&lt;/li&gt;
&lt;li&gt;The Comparison That Matters&lt;/li&gt;
&lt;li&gt;The "Pure Event Loop" Lie&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;What the Experiments Showed&lt;/li&gt;
&lt;li&gt;The Fix Reveals the Lesson&lt;/li&gt;
&lt;li&gt;What This Phase Actually Teaches&lt;/li&gt;
&lt;li&gt;Where I'm Still Fuzzy&lt;/li&gt;
&lt;li&gt;Try It Yourself&lt;/li&gt;
&lt;li&gt;Next&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;A server has CPU, memory, disk, and network capacity. When it fails under load, which one ran out — and does the answer change what you do about it?&lt;/p&gt;

&lt;p&gt;Phase 1 is an argument that it matters enormously, and that most engineers are watching the wrong gauges.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Building Blocks: What Actually Handles a Connection
&lt;/h2&gt;

&lt;p&gt;Before getting to failure modes, it helps to have a concrete model of what happens when a user connects to a server.&lt;/p&gt;

&lt;h3&gt;
  
  
  File Descriptors
&lt;/h3&gt;

&lt;p&gt;The OS tracks every open resource — files, network connections, pipes — using a small integer called a &lt;strong&gt;file descriptor (FD)&lt;/strong&gt;. The first three are always reserved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FD 0 → stdin  (keyboard input)
FD 1 → stdout (console output)
FD 2 → stderr (error output)
FD 3 → your log file
FD 4 → user A's connection
FD 5 → user B's connection
...
FD 1024 → OS says "EMFILE — no more" ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every TCP connection your server accepts consumes one FD. The default per-process limit on Linux is &lt;strong&gt;1,024&lt;/strong&gt;. When that fills, the OS returns &lt;code&gt;EMFILE&lt;/code&gt; (too many open files) and refuses new connections — regardless of how much CPU or memory you have left.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;        &lt;span class="c"&gt;# check current limit → probably 1024&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 65535  &lt;span class="c"&gt;# raise it (current session only)&lt;/span&gt;
&lt;span class="c"&gt;# Permanent: edit /etc/security/limits.conf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TCP Connections: The Handshake
&lt;/h3&gt;

&lt;p&gt;Every HTTP connection starts with a TCP three-way handshake before any data flows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Server: SYN      ("I'd like to connect")
Server → Client: SYN-ACK  ("Got it, I'm ready")
Client → Server: ACK       ("Confirmed — send your request")
Client → Server: [data]    (the actual HTTP request)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step takes a network round-trip. For a client in the same data center that's ~0.1ms; across continents it's 100–300ms. This overhead motivated &lt;strong&gt;HTTP Keep-Alive&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep-Alive: Efficiency With a Hidden Cost
&lt;/h3&gt;

&lt;p&gt;HTTP Keep-Alive reuses a single TCP connection for multiple requests instead of handshaking for each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without: handshake → request → close,  handshake → request → close  (2 handshakes)
With:    handshake → request → request → request → close             (1 handshake)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Faster for the client. But idle keep-alive connections still hold an FD open on the server doing nothing. 5,000 users sitting on an idle browser tab = 5,000 FDs consumed. This is a trap that ended the startup in the war story below.&lt;/p&gt;

&lt;h3&gt;
  
  
  TIME_WAIT: The Port Cooldown
&lt;/h3&gt;

&lt;p&gt;When a TCP connection closes, the port on the &lt;em&gt;client&lt;/em&gt; side enters &lt;strong&gt;TIME_WAIT&lt;/strong&gt; for 60–120 seconds. During this window, that port can't be reused for new outbound connections.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,000 outbound connections/sec × 60 sec TIME_WAIT = 60,000 ports locked
Total available ephemeral ports: ~65,000
→ Port exhaustion, even with healthy CPU and memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters most on servers that make many outbound connections (proxies, services that call many APIs). You'll see &lt;code&gt;connect: Cannot assign requested address&lt;/code&gt; in logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Resource Landscape
&lt;/h2&gt;

&lt;p&gt;A server has five independent ceilings, each with its own failure signature:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;What fills it&lt;/th&gt;
&lt;th&gt;Failure signature&lt;/th&gt;
&lt;th&gt;Default limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File descriptors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TCP connections, open files&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EMFILE&lt;/code&gt; — new connections refused&lt;/td&gt;
&lt;td&gt;1,024/process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heap, thread stacks, buffers&lt;/td&gt;
&lt;td&gt;OOM Killer silently kills process&lt;/td&gt;
&lt;td&gt;Depends on RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Computation, context switching&lt;/td&gt;
&lt;td&gt;All requests slow down together&lt;/td&gt;
&lt;td&gt;100% across cores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk IOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read/write operations&lt;/td&gt;
&lt;td&gt;Write latency spikes&lt;/td&gt;
&lt;td&gt;SSD: ~100K–500K/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network ports&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Outbound connections (TIME_WAIT)&lt;/td&gt;
&lt;td&gt;New outbound connections fail&lt;/td&gt;
&lt;td&gt;~65K ephemeral ports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The critical distinction: &lt;strong&gt;CPU and memory saturation cause gradual degradation&lt;/strong&gt; — everything slows down proportionally. &lt;strong&gt;FD and port exhaustion cause total failure&lt;/strong&gt; — new connections are refused while existing ones and dashboard metrics look completely healthy. The second kind is the one that produces a 47-minute outage where nobody can figure out why the server is "down."&lt;/p&gt;




&lt;h2&gt;
  
  
  Concurrency Models: How the Server Handles Multiple Users
&lt;/h2&gt;

&lt;p&gt;How a server handles multiple simultaneous connections determines which ceiling it hits first. There are three main approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thread-per-Connection
&lt;/h3&gt;

&lt;p&gt;A new OS thread is created for each incoming connection. That thread handles everything for that connection — reading the request, processing it, writing the response — then terminates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A connects → Thread 1 created (1MB RAM)
User B connects → Thread 2 created (1MB RAM)
User C connects → Thread 3 created (1MB RAM)
...
User 32,000 → Thread 32,000 → OOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple to code: each thread runs straight-line blocking code. But each thread costs ~1MB of stack space, and &lt;strong&gt;context switching&lt;/strong&gt; between thousands of threads burns CPU. At 10,000 threads, 10–30% of CPU goes just to saving and restoring thread state. At 100,000 — the system is unusable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First ceiling:&lt;/strong&gt; Memory (32GB ÷ 1MB/thread ≈ 32,000 max threads), then CPU from context switching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Loop
&lt;/h3&gt;

&lt;p&gt;One thread handles all connections by processing callbacks as they become ready. Instead of blocking while waiting for I/O, it registers a callback and moves on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hotel receptionist analogy:
  Loop forever:
    1. Any new guests?        → start check-in
    2. Any responses ready?   → send them
    3. Any room service done? → notify the room (non-blocking)
    4. Back to step 1

ONE receptionist. MANY guests. Never stands idle waiting.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each connection costs only a few KB (just a file descriptor and some socket state), not 1MB. The same 32GB server can hold 100,000+ connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First ceiling:&lt;/strong&gt; File descriptors (OS limit), or CPU if any one request does synchronous computation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness:&lt;/strong&gt; Head-of-line blocking. One slow synchronous task holds the single thread, freezing every other connection behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Drive-through analogy:
  Car A: "Custom birthday cake" (10 min)
  Car B: "Just a coffee"  ← waits 10 minutes behind Car A
  Car C: "A muffin"       ← waits 10:30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Thread Pool
&lt;/h3&gt;

&lt;p&gt;A fixed number of pre-created threads pull work from a shared queue. Balances the two extremes: bounded memory cost (N threads, not one per connection), and true parallelism across CPU cores.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hire 20 waiters. All new tables wait in the lobby.
A free waiter → grabs next table → serves → returns to lobby.
If all 20 are busy → table waits in queue.
If queue is full → reject with 503.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pool size formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pool_size = num_cores × (1 + wait_time / compute_time)
Example: 8 cores, 90ms DB wait, 10ms compute → 8 × 10 = 80 threads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode:&lt;/strong&gt; If a slow downstream dependency holds all threads (e.g., 200 threads × 30-second timeout = all blocked), the queue fills and the service rejects everything — even though your code is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Comparison That Matters
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Thread-per-Connection&lt;/th&gt;
&lt;th&gt;Event Loop&lt;/th&gt;
&lt;th&gt;Thread Pool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory @ 10K connections&lt;/td&gt;
&lt;td&gt;~10 GB&lt;/td&gt;
&lt;td&gt;~50 MB&lt;/td&gt;
&lt;td&gt;~500 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max practical connections&lt;/td&gt;
&lt;td&gt;1K–10K&lt;/td&gt;
&lt;td&gt;10K–100K+&lt;/td&gt;
&lt;td&gt;10K–50K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;OOM / context-switch death spiral&lt;/td&gt;
&lt;td&gt;Head-of-line blocking&lt;/td&gt;
&lt;td&gt;Queue overflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Race conditions&lt;/td&gt;
&lt;td&gt;Every CPU instruction boundary&lt;/td&gt;
&lt;td&gt;Only at &lt;code&gt;await&lt;/code&gt; boundaries&lt;/td&gt;
&lt;td&gt;Every CPU instruction boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who uses it&lt;/td&gt;
&lt;td&gt;Apache httpd&lt;/td&gt;
&lt;td&gt;Node.js, nginx, Redis&lt;/td&gt;
&lt;td&gt;Java Tomcat, Go/Netty&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The "Pure Event Loop" Lie
&lt;/h3&gt;

&lt;p&gt;Node.js is described as single-threaded. It isn't, exactly. The event loop handles network I/O directly (non-blocking), but &lt;strong&gt;file I/O and DNS lookups are blocking operations&lt;/strong&gt; that can't be done asynchronously by the OS. Node offloads these to a small internal thread pool via libuv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event loop handles:          libuv thread pool (default: 4 threads) handles:
  TCP connections              fs.readFile / writeFile
  HTTP parsing                 DNS lookups (dns.lookup)
  DB queries over network      crypto (pbkdf2, scrypt)
  setTimeout / setInterval     zlib compression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 4-thread default is dangerously low in production. If 4 file reads are in progress, a DNS lookup queues behind them. HTTP calls to external APIs start timing out for no obvious reason. The fix: &lt;code&gt;UV_THREADPOOL_SIZE=64&lt;/code&gt; set before the process starts.&lt;/p&gt;

&lt;p&gt;Every "event loop" runtime is actually a hybrid. Knowing which operations go to the thread pool and which stay on the event loop is essential for debugging latency spikes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Experiments Showed
&lt;/h2&gt;

&lt;p&gt;The prototype: a Node.js HTTP key-value store, raw &lt;code&gt;http&lt;/code&gt; module, zero npm packages. Four routes: &lt;code&gt;/data/:key&lt;/code&gt; (read/write), &lt;code&gt;/stats&lt;/code&gt; (live metrics — memory, FDs, event loop lag), and &lt;code&gt;/heavy&lt;/code&gt; (a deliberate CPU blocker). Simple enough to break in predictable ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 1 — Connection flood: the ceiling was the OS kernel
&lt;/h3&gt;

&lt;p&gt;Opening TCP connections without closing them, stepping up in batches. The server stayed alive until:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15,965 connections → ENOBUFS (no buffer space available)
CPU: ~0%    Node heap: ~480 MB (healthy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event loop was not blocked. Node's memory was fine. What ran out was &lt;strong&gt;OS kernel network buffer space&lt;/strong&gt; — roughly 250 MB of non-paged pool memory consumed by socket receive/send buffers (~16 KB each). The ceiling was in the kernel, not in the application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ceiling hierarchy hit in order:
  1. OS kernel network buffers  ← actual ceiling on Windows
  2. FD limit (~16K on Windows) ← would have been next
  3. Node.js heap               ← never reached
  4. CPU                        ← never reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the war story that opens the phase: a social media API during a viral spike. CPU at 5%, memory at 40%, every dashboard looked healthy — but the server was refusing every new connection. 47-minute outage. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 65535   &lt;span class="c"&gt;# raises the per-process FD limit from the default 1,024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point isn't to memorize &lt;code&gt;ulimit&lt;/code&gt;. It's that the server had massive headroom on four of its five ceilings, and failed on the fifth one nobody was measuring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 2 — One CPU-bound request freezes all 15,965 users
&lt;/h3&gt;

&lt;p&gt;A single request to &lt;code&gt;/heavy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This holds the event loop thread — nothing else can run&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During those 5 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/stats response time:  5,000 ms  (normally 1 ms)
Event loop lag:        12,000 ms
CPU:                   98%
All other requests:    frozen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/stats&lt;/code&gt; route does trivial work — read process memory, count handles, respond. Under normal conditions it takes 1ms. The while loop on &lt;code&gt;/heavy&lt;/code&gt; blocked it for 5 full seconds because &lt;strong&gt;the event loop is a single thread with cooperative scheduling&lt;/strong&gt;. There is no OS scheduler interrupting a running synchronous task to let another request proceed. The CPU-bound route held the thread until it finished.&lt;/p&gt;

&lt;p&gt;This surfaces in production from: &lt;code&gt;JSON.parse()&lt;/code&gt; on a 50 MB payload, an unanchored regex on user input (ReDoS), or a forgotten &lt;code&gt;fs.readFileSync()&lt;/code&gt; in a request handler. All of them stall every connected user simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 3 — Async doesn't make reads safe
&lt;/h3&gt;

&lt;p&gt;100 concurrent PUT requests, each reading the current counter, incrementing it, and writing it back. Expected final value: 100. Actual: 60-something.&lt;/p&gt;

&lt;p&gt;The interleaving that causes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A: await readStore()   → gets { counter: 1 }  ← yields here
Request B: await readStore()   → gets { counter: 1 }  ← stale; A hasn't written
Request A: await writeStore({ counter: 2 })
Request B: await writeStore({ counter: 2 })            ← overwrites; should be 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assumption going in: single-threaded event loop means no interleaving. That's true &lt;em&gt;between&lt;/em&gt; &lt;code&gt;await&lt;/code&gt; calls — code between two awaits is atomic. But the full async function is not. Every &lt;code&gt;await&lt;/code&gt; is a yield point where the event loop can run another callback. In a read-modify-write pattern, that's enough.&lt;/p&gt;

&lt;p&gt;The same lost-update problem exists in multi-threaded code, just at every CPU instruction instead of only at &lt;code&gt;await&lt;/code&gt; boundaries. The event loop makes races less frequent, not impossible.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix Reveals the Lesson
&lt;/h2&gt;

&lt;p&gt;Each experiment has a distinct fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FD/buffer exhaustion&lt;/strong&gt; → raise &lt;code&gt;ulimit -n&lt;/code&gt; / set &lt;code&gt;nofile&lt;/code&gt; in systemd unit or container config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event loop blocking&lt;/strong&gt; → offload synchronous CPU work to &lt;code&gt;worker_threads&lt;/code&gt;; never call blocking APIs in a hot path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async race conditions&lt;/strong&gt; → serialize writes through a queue, or use atomic DB operations (transactions, compare-and-swap)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three experiments produced similar surface symptoms — slow or unresponsive server. The root causes are completely different. Applying the wrong intervention (or adding a second server) wouldn't have helped. The resource that fails determines both the symptom and the remedy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Phase Actually Teaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not all resource ceilings behave like slowness.&lt;/strong&gt; FD and port exhaustion produce hard refusals while CPU and memory metrics look healthy. This class of failure is disproportionately misdiagnosed because teams are watching the wrong gauges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concurrency model determines which ceiling you hit first.&lt;/strong&gt; Thread-per-connection hits memory first (~1MB/thread → ~32K threads on 32GB RAM). Event-loop hits file descriptors or CPU-bound blocking first. The model isn't just a performance choice — it changes the failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Phase Actually Teaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A server has five independent resource ceilings, and the smallest one is the actual limit.&lt;/strong&gt; CPU saturation causes gradual degradation. FD exhaustion and port exhaustion cause total failure while all other metrics look fine. Know which ceiling you're approaching before an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The thread-per-connection model kills itself on memory and context switching.&lt;/strong&gt; At 10,000 threads: ~10 GB of stack space, and 10–30% of CPU wasted on saving/restoring thread state. This is why Node.js, nginx, and Redis all chose the event loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The event loop trades memory efficiency for CPU vulnerability.&lt;/strong&gt; A single synchronous task holds the thread. Head-of-line blocking isn't a bug to fix — it's a fundamental property of cooperative concurrency. The architecture choice is: which failure mode can I tolerate? OOM death spiral, or one hot request freezing everyone?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. "Single-threaded" is a simplification.&lt;/strong&gt; Node.js has 4 libuv worker threads by default handling file I/O, DNS, and crypto. DNS lookups queue behind file reads. In production, set &lt;code&gt;UV_THREADPOOL_SIZE=64&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Async code has race conditions at await boundaries.&lt;/strong&gt; Code between two awaits is atomic. A full async function is not. Every read-modify-write pattern across awaits needs explicit serialization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use which model:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt; 5K connections, simple code needed        → Thread-per-connection
&amp;gt; 10K connections, mostly I/O-bound         → Event loop
Mixed CPU + I/O, need parallelism           → Thread pool
Production at scale                         → Hybrid (event loop + worker pool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where I'm Still Fuzzy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;ENOBUFS&lt;/code&gt; vs &lt;code&gt;EMFILE&lt;/code&gt;&lt;/strong&gt; — I hit &lt;code&gt;ENOBUFS&lt;/code&gt; at ~16K connections on Windows, which maps to OS kernel buffer exhaustion, not the FD limit. On Linux with default &lt;code&gt;ulimit -n 1024&lt;/code&gt;, &lt;code&gt;EMFILE&lt;/code&gt; would appear much earlier. Are these always separable in production?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TIME_WAIT at high connection churn&lt;/strong&gt; — I understand port exhaustion in theory (1K conns/sec × 60s = 60K locked ports), but I haven't built something that actually hits it. That would require a high-outbound-connection scenario — probably relevant in Phase 9 (service decomposition) when services call each other.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;libuv thread pool saturation in practice&lt;/strong&gt; — the 4-thread default is clearly too low, but what does the performance curve look like as you raise it? Is there a point where more threads hurt?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Four experiments worth running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET /stats&lt;/code&gt; — see live FD count, heap usage, and event loop lag&lt;/li&gt;
&lt;li&gt;Hit &lt;code&gt;/heavy&lt;/code&gt; in one tab, then hit &lt;code&gt;/stats&lt;/code&gt; from another — watch the lag number&lt;/li&gt;
&lt;li&gt;Run 100 concurrent writes to a counter and check if the final value matches the expected count&lt;/li&gt;
&lt;li&gt;Open thousands of connections without closing them and watch when the OS complains first&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The stats endpoint measures event loop lag by scheduling a &lt;code&gt;setTimeout(0)&lt;/code&gt; callback and measuring how long before it actually runs. Under normal load: &amp;lt; 5ms. During a CPU-bound request: in the thousands.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Phase 2 moves the problem to multiple machines — which immediately surfaces a new class of failures that single-machine thinking doesn't predict.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of a 12-phase series building distributed systems intuition from first principles. Each phase has a running prototype, failure scenarios, and a gate check before moving on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>beginners</category>
      <category>learning</category>
    </item>
    <item>
      <title>System Design the Agentic Way — How I'm Using AI Agents to Actually Learn Distributed Systems</title>
      <dc:creator>HappyBug</dc:creator>
      <pubDate>Mon, 13 Jul 2026 06:30:28 +0000</pubDate>
      <link>https://dev.to/happybug/system-design-the-agentic-way-how-im-using-ai-agents-to-actually-learn-distributed-systems-529f</link>
      <guid>https://dev.to/happybug/system-design-the-agentic-way-how-im-using-ai-agents-to-actually-learn-distributed-systems-529f</guid>
      <description>&lt;h2&gt;
  
  
  System Design the Agentic Way
&lt;/h2&gt;

&lt;p&gt;I wanted to learn system design. Not "memorize CAP theorem for interviews" learn — actually understand why things break at scale and what to do about it.&lt;/p&gt;

&lt;p&gt;The problem: most resources give you theory without pain. You read about load balancers without ever watching one fail. You learn about replication without seeing stale data appear in front of you.&lt;/p&gt;

&lt;p&gt;So I built something different.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Idea
&lt;/h3&gt;

&lt;p&gt;What if I had a pair-programmer that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts every topic with a &lt;strong&gt;real production failure&lt;/strong&gt; (so I feel why it matters)&lt;/li&gt;
&lt;li&gt;Makes me &lt;strong&gt;predict&lt;/strong&gt; what will happen before I run anything&lt;/li&gt;
&lt;li&gt;Forces me to &lt;strong&gt;build&lt;/strong&gt; minimal prototypes, not just read&lt;/li&gt;
&lt;li&gt;Then makes me &lt;strong&gt;break them&lt;/strong&gt; deliberately&lt;/li&gt;
&lt;li&gt;And finally &lt;strong&gt;tests my judgment&lt;/strong&gt; with ambiguous 3am scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's what I built — using GitHub Copilot's custom agents and skills as a structured teaching system.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Curriculum
&lt;/h3&gt;

&lt;p&gt;12 phases. Each one builds on the previous. No skipping.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What Goes Wrong Without It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Single Machine Ceiling&lt;/td&gt;
&lt;td&gt;Your server hits 15K connections and starts refusing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Stateless Horizontal Scaling&lt;/td&gt;
&lt;td&gt;You deploy and lose 47 payments during the restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Data Storage Trade-offs&lt;/td&gt;
&lt;td&gt;Redis crashes and your last 30 seconds of writes vanish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Caching &amp;amp; Invalidation&lt;/td&gt;
&lt;td&gt;Users see stale prices and buy at the wrong amount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Async Processing &amp;amp; Queues&lt;/td&gt;
&lt;td&gt;Payment service is down for 2 seconds, 500 orders lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Replication &amp;amp; Consistency&lt;/td&gt;
&lt;td&gt;Read replicas serve yesterday's data as if it's current&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Partitioning &amp;amp; Sharding&lt;/td&gt;
&lt;td&gt;One customer has 90% of the data, hot partition melts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Coordination &amp;amp; Consensus&lt;/td&gt;
&lt;td&gt;Two nodes both think they're the leader, split-brain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Service Decomposition&lt;/td&gt;
&lt;td&gt;Distributed transaction fails halfway, money vanishes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10. Observability&lt;/td&gt;
&lt;td&gt;Something is wrong. You have no idea what.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11. Resilience Patterns&lt;/td&gt;
&lt;td&gt;One slow dependency freezes your entire service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12. Capacity Planning&lt;/td&gt;
&lt;td&gt;Black Friday hits 10x predictions, everything collapses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  The Method
&lt;/h3&gt;

&lt;p&gt;Every phase follows the same loop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Motivate → Model → Decide → Build → Break → Gate&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔥 War Story    → Feel the pain
🧠 Concepts     → Understand the mechanism
⚖️ Trade-off   → Pick an approach (and own the downsides)
🔨 Build       → Minimal prototype with real code
💥 Break It    → Deliberately kill components
🎯 Gate Check  → Scenario-based judgment test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "Break It" step is where most of the learning happens. My prediction is almost always wrong the first time. That gap — between what I expected and what actually happened — is the lesson.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Tooling
&lt;/h3&gt;

&lt;p&gt;The whole system runs inside VS Code using GitHub Copilot's custom agent features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 agents&lt;/strong&gt; — instructor, gate-keeper, adversary, reviewer, orchestrator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 skills&lt;/strong&gt; — war stories, concept deep-dives, prototype specs, failure labs, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge files&lt;/strong&gt; — real production patterns, failure catalogs, anti-patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning journal&lt;/strong&gt; — auto-captured insights, predictions, and mistakes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agents don't give me answers. They ask me to predict, let me fail, then explain why.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where I Am
&lt;/h3&gt;

&lt;p&gt;I've completed Phase 1 (single machine limits) and Phase 2 (horizontal scaling with Docker + Nginx + Redis). In the next posts, I'll walk through each phase — what I built, what I broke, and what surprised me.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try It Yourself
&lt;/h3&gt;

&lt;p&gt;The full system is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://github.com/aishwarya-chamanoor/system-design-agenticway" rel="noopener noreferrer"&gt;system-design-agenticway&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What I'd Love From You
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ Star the repo if this approach resonates&lt;/li&gt;
&lt;li&gt;🐛 Open issues if something's broken or confusing&lt;/li&gt;
&lt;li&gt;💡 Suggest improvements to the curriculum&lt;/li&gt;
&lt;li&gt;📝 Tell me: how do YOU learn system design? What works? What doesn't?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: &lt;strong&gt;Phase 1 — The Single Machine Ceiling.&lt;/strong&gt; I'll show you what happens when 15,965 TCP connections hit one Node.js process, and what it taught me about operating systems.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow for the full series. Each post covers one phase of the journey.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Clone it, open in VS Code with Copilot, and start Phase 1. All you need is Node.js and Docker.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Love From You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ Star the repo if this approach resonates&lt;/li&gt;
&lt;li&gt;🐛 Open issues if something's broken or confusing&lt;/li&gt;
&lt;li&gt;💡 Suggest improvements to the curriculum&lt;/li&gt;
&lt;li&gt;📝 Tell me: how do YOU learn system design? What works? What doesn't?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: &lt;strong&gt;Phase 1 — The Single Machine Ceiling.&lt;/strong&gt; I'll show you what happens when 15,965 TCP connections hit one Node.js process, and what it taught me about operating systems.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow for the full series. Each post covers one phase of the journey.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>ai</category>
      <category>beginners</category>
      <category>learning</category>
    </item>
    <item>
      <title>How are people collaborating here for dev challenges? Need advice :)</title>
      <dc:creator>HappyBug</dc:creator>
      <pubDate>Thu, 04 Jun 2026 09:18:12 +0000</pubDate>
      <link>https://dev.to/happybug/how-are-people-collaborating-here-for-dev-challenges-need-advice--1abp</link>
      <guid>https://dev.to/happybug/how-are-people-collaborating-here-for-dev-challenges-need-advice--1abp</guid>
      <description></description>
      <category>community</category>
      <category>devchallenge</category>
      <category>discuss</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
