<?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: Yathiskumar</title>
    <description>The latest articles on DEV Community by Yathiskumar (@yathiskumar).</description>
    <link>https://dev.to/yathiskumar</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%2F3950226%2Fec1a62eb-1975-4319-897a-30f8796138e6.png</url>
      <title>DEV Community: Yathiskumar</title>
      <link>https://dev.to/yathiskumar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yathiskumar"/>
    <language>en</language>
    <item>
      <title>vLLM reinvented the operating system, and nobody told you</title>
      <dc:creator>Yathiskumar</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:49:32 +0000</pubDate>
      <link>https://dev.to/yathiskumar/vllm-reinvented-the-operating-system-and-nobody-told-you-ja8</link>
      <guid>https://dev.to/yathiskumar/vllm-reinvented-the-operating-system-and-nobody-told-you-ja8</guid>
      <description>&lt;p&gt;A few weeks ago I was reading the vLLM scheduler source, and I got the specific kind of déjà vu you get when you walk into a stranger's house and their kitchen is laid out exactly like your grandmother's.&lt;/p&gt;

&lt;p&gt;There was a block table. There was a free list. There was a preemption path that could either swap a victim out to host memory or throw its work away and recompute it later. There was a fallback for when memory ran out mid-flight.&lt;/p&gt;

&lt;p&gt;I had read this code before. In 2009. It was called an operating systems textbook.&lt;/p&gt;

&lt;p&gt;This is not a criticism — it's the highest possible compliment, and it's also the most useful thing I can tell you if you're an engineer who feels like the ground moved under you in the last eighteen months. "AI infrastructure" sounds like a field you have to start over in. It mostly isn't. The dominant problems in LLM serving are memory fragmentation, cache affinity, queue scheduling, and rate limiting — and if you have ever tuned a JVM, sized a Redis cluster, or debugged a p99 that only spiked at 4pm, you already own the concepts.&lt;/p&gt;

&lt;p&gt;You just have to see the translation table. So here it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this suddenly matters
&lt;/h2&gt;

&lt;p&gt;The reason I'm writing this now rather than a year ago is that the interview loop changed.&lt;/p&gt;

&lt;p&gt;For years, "design a system that serves an ML model" was a question you got if you applied for an ML infrastructure role. In 2026 it turns up in general backend loops — "design a customer support chatbot on top of a third-party LLM," "design safeguards for an agent that can take actions for a user," "walk me through serving a 70B model to 10,000 concurrent users." The &lt;a href="https://www.systemdesignhandbook.com/blog/ai-system-design-interview-questions/" rel="noopener noreferrer"&gt;guides tracking these loops&lt;/a&gt; describe the same shift: AI scenarios have moved out of specialist interviews and into standard ones, and cost-efficiency is now graded alongside latency and throughput rather than treated as a bonus.&lt;/p&gt;

&lt;p&gt;That last one is the tell. Cost-and-operations grading is what happens when a technology stops being a research demo and becomes a line item. And when something becomes a line item, the questions asked about it stop being novel and start being &lt;em&gt;the questions we have always asked about expensive machines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Which is good news for you. Let's go through the four big ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. PagedAttention is virtual memory. Actually, literally.
&lt;/h2&gt;

&lt;p&gt;Start with the problem, because the problem is the fun part.&lt;/p&gt;

&lt;p&gt;When an LLM generates text, it keeps a running scratchpad for every request called the KV cache — the key and value tensors for every token processed so far. It grows by one entry per token, on every single step, for the entire life of the request. It is large: for a big model with a long context, we're talking gigabytes per request, living on a GPU that has maybe 80 of them.&lt;/p&gt;

&lt;p&gt;Here's the trap. Early serving systems stored each request's KV cache in one contiguous slab. Contiguous allocation needs a size up front. But you don't &lt;em&gt;know&lt;/em&gt; the size up front — you have no idea whether the model will produce 12 tokens or 4,000. So the only safe move was to allocate for the maximum: reserve 2,048 tokens' worth of GPU memory for a request that might write 30.&lt;/p&gt;

&lt;p&gt;If you have ever sized a buffer for the worst case, you already know how this ends. The &lt;a href="https://arxiv.org/abs/2309.06180" rel="noopener noreferrer"&gt;vLLM paper&lt;/a&gt; measured it: existing systems were using only about 20–38% of their KV cache memory for actual token state. The other 60–80% was fragmentation and over-reservation. Two thirds of the most expensive RAM on earth, sitting empty, reserved for tokens that were never generated.&lt;/p&gt;

&lt;p&gt;You have seen this bug. It is &lt;em&gt;the&lt;/em&gt; allocator bug. Fixed-size slots waste the tail of every allocation (internal fragmentation); variable-size slots leave unusable gaps between live objects (external fragmentation); and the classic escape hatch is to stop demanding that a logical object be physically contiguous at all.&lt;/p&gt;

&lt;p&gt;Which is exactly what PagedAttention does. Chop the KV cache into fixed-size blocks — say 16 tokens each. Scatter those blocks anywhere in GPU memory. Keep a &lt;strong&gt;block table&lt;/strong&gt; mapping each sequence's logical block &lt;em&gt;i&lt;/em&gt; to whatever physical block actually holds it. The attention kernel gets taught to gather across that table.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure omitted&lt;/strong&gt; — &lt;a href="https://subroute.dev/blog/llm-serving-is-the-systems-you-already-know" rel="noopener noreferrer"&gt;see the diagram in the original post&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you swap four nouns, that paragraph is from a 1970s paper on demand paging. Logical blocks are pages. The block table is a page table. The GPU allocator is the frame allocator. And the payoff is the payoff paging always gives you: internal fragmentation is bounded to &lt;em&gt;at most one partial block per sequence&lt;/em&gt; instead of one giant wasted reservation, and external fragmentation disappears entirely because every block is interchangeable. Waste drops to a few percent. More requests fit in memory. More requests in memory means bigger batches, and bigger batches on a GPU means throughput — the paper reports 2–4× against the state of the art at the time, at equal latency.&lt;/p&gt;

&lt;p&gt;Then it gets better, in a way that will make you grin if you've ever implemented copy-on-write. Two requests that share a system prompt have identical KV state for that prefix. Identical state, block table indirection, refcounts... so of course vLLM shares the physical blocks between them and only copies when one diverges. &lt;code&gt;fork()&lt;/code&gt; for transformers.&lt;/p&gt;

&lt;p&gt;And when GPU memory runs out mid-generation — because a batch of requests all decided to be chatty at once — the engine preempts. It picks victims and either swaps their blocks out to CPU memory or discards them and recomputes from the prompt later. Swap or recompute. That is a page-replacement policy with an unusually honest cost model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to &lt;em&gt;feel&lt;/em&gt; rather than read this: the &lt;a href="https://subroute.dev/topics/memory-allocation" rel="noopener noreferrer"&gt;memory allocation playground&lt;/a&gt; lets you watch first-fit, best-fit, and buddy allocators shred a heap into unusable holes, and the &lt;a href="https://subroute.dev/topics/page-replacement" rel="noopener noreferrer"&gt;page replacement simulator&lt;/a&gt; lets you run FIFO, LRU, Clock, and OPT against the same reference string and watch FIFO commit Bélády's anomaly in public. Same mechanics, smaller numbers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Prefix-aware routing is consistent hashing, rediscovered under pressure
&lt;/h2&gt;

&lt;p&gt;Now scale it out. One GPU becomes a fleet.&lt;/p&gt;

&lt;p&gt;Your instinct — a good instinct, earned honestly — is round-robin or least-connections in front of N identical replicas. Stateless workers, uniform work, spread it evenly. This is correct for essentially every web service you have ever operated.&lt;/p&gt;

&lt;p&gt;It is quietly terrible here, and the reason is that your replicas are not stateless. Each one has a warm KV cache full of prefixes it has already computed.&lt;/p&gt;

&lt;p&gt;Picture a support-bot deployment. Every request carries the same 6,000-token system prompt — policies, tone, tool definitions, few-shot examples. Processing those 6,000 tokens is the &lt;em&gt;prefill&lt;/em&gt; phase, and it is real compute. If a request lands on a replica that already has that prefix cached, prefill is a table lookup and the user sees a first token almost immediately. If it lands on a cold replica, the GPU grinds through 6,000 tokens of attention before emitting a single character.&lt;/p&gt;

&lt;p&gt;Round-robin guarantees that every replica ends up caching every prefix, which means your effective cache size is &lt;em&gt;the size of one replica&lt;/em&gt; no matter how many you buy. You have built an N-node cluster with 1 node's worth of cache. Meanwhile the requests bounce between them, evicting each other's work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure omitted&lt;/strong&gt; — &lt;a href="https://subroute.dev/blog/llm-serving-is-the-systems-you-already-know" rel="noopener noreferrer"&gt;see the diagram in the original post&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix is the thing you already know: &lt;strong&gt;route by content, not by counter.&lt;/strong&gt; Hash the prefix, send matching prefixes to the same replica, let each node specialize. Cache affinity. That is consistent hashing wearing a name badge that says "prefix-aware routing," and the ecosystem converged on it from three directions at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SGLang's router&lt;/strong&gt; tracks approximate prefix locality with a radix tree and falls back to shortest-queue routing when nodes get imbalanced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GKE's Inference Gateway&lt;/strong&gt; hashes the incoming token prefix and picks the replica most likely to hold it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://llm-d.ai/blog/kvcache-wins-you-can-see" rel="noopener noreferrer"&gt;llm-d&lt;/a&gt;&lt;/strong&gt; goes exact: every vLLM pod publishes KV-cache events, the router maintains an index keyed by block hash, filters candidates down to pods that actually hold the prefix, and picks the least token-loaded pod &lt;em&gt;within that set&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that last one again, because the two-stage structure is the whole lesson. Filter by cache locality, &lt;em&gt;then&lt;/em&gt; balance load inside the filtered set. Pure locality routing creates hotspots — one viral prefix melts one pod. Pure load balancing throws away the cache. You need both, and "both" means locality as a filter and load as a tiebreaker.&lt;/p&gt;

&lt;p&gt;Their published benchmark is eye-watering and I want to be precise about it rather than let you take home a number that doesn't survive contact with your workload: on 8 vLLM pods across 16 H100s, simulating 150 tenants with 6,000-token contexts, precise prefix-aware scheduling hit a P90 time-to-first-token of &lt;strong&gt;0.54 seconds versus 31 seconds&lt;/strong&gt; for approximate routing and &lt;strong&gt;92 seconds&lt;/strong&gt; for random. That's the 57× headline. Throughput roughly doubled against cache-blind configs.&lt;/p&gt;

&lt;p&gt;The caveat is doing a lot of work: total KV demand was 73% of cluster capacity — deliberate, heavy cache pressure, six times what any single pod could hold. Under light load with a tiny shared prefix, the gap shrinks toward nothing. Which is itself the familiar lesson: &lt;strong&gt;cache-aware routing wins exactly when the cache is scarce and the workload is skewed&lt;/strong&gt;, and if you've ever argued about whether to shard by user ID, you have had this exact argument before.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;a href="https://subroute.dev/topics/consistent-hashing" rel="noopener noreferrer"&gt;consistent hashing simulator&lt;/a&gt; is the fastest way to internalize the failure mode — add and remove nodes, watch how much of the keyspace remaps, and see why virtual nodes exist. The &lt;a href="https://subroute.dev/topics/load-balancing" rel="noopener noreferrer"&gt;load balancing playground&lt;/a&gt; runs round-robin, least-connections, EWMA, and power-of-two-choices against one shared request stream so you can watch a skewed workload wreck a naive policy in real time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Continuous batching is a scheduler fighting head-of-line blocking
&lt;/h2&gt;

&lt;p&gt;Third problem, and this one is pure queueing theory.&lt;/p&gt;

&lt;p&gt;GPUs want big batches — that's how you amortize weight loading against arithmetic. So batch the requests. The naive version, &lt;strong&gt;static batching&lt;/strong&gt;, collects N requests, runs them together until every one has finished generating, then starts the next batch.&lt;/p&gt;

&lt;p&gt;You have already spotted it. Request A wants 20 tokens, request B wants 2,000. A finishes at step 20 and its slot sits there, occupied and idle, burning GPU for 1,980 steps while B rambles. And every request that arrived at step 21 waits in line behind B for no reason at all.&lt;/p&gt;

&lt;p&gt;That is head-of-line blocking. Same phenomenon as one slow query pinning a connection pool, one fat HTTP/1.1 response stalling a pipelined connection, one giant job hogging a thread pool. Long job in front, short jobs starving behind it, utilization on the floor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure omitted&lt;/strong&gt; — &lt;a href="https://subroute.dev/blog/llm-serving-is-the-systems-you-already-know" rel="noopener noreferrer"&gt;see the diagram in the original post&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Continuous batching&lt;/strong&gt; — the design shared by vLLM, TGI, and SGLang — fixes it by making the batch composition mutable &lt;em&gt;every single decoding step&lt;/em&gt;. A request finishes, it leaves immediately. A slot opens, a queued request joins mid-flight. The batch is a living set, not a cohort. Iteration-level scheduling rather than request-level.&lt;/p&gt;

&lt;p&gt;This has a prerequisite, and the prerequisite is section 1. You can only admit new work every step if you can hand out memory in small increments to a request whose final size you don't know — which is precisely what paged KV blocks give you. And you need preemption for the moment where you've admitted more work than will fit and something has to be evicted mid-generation. Paging and scheduling are co-dependent here, exactly as they are in an OS.&lt;/p&gt;

&lt;p&gt;From there the research goes exactly where an OS person would guess. &lt;a href="https://vllm.ai/blog/2025-09-05-anatomy-of-vllm" rel="noopener noreferrer"&gt;Chunked prefill&lt;/a&gt; splits a huge prompt into pieces and interleaves them with ongoing decodes, so one 100k-token prompt doesn't stall everyone else — that's time-slicing a long-running job. FastServe assigns priority by prompt length using a skip-join multi-level feedback queue, which is MLFQ, unmodified, from the same textbook. Others are chasing predictive shortest-job-first by &lt;em&gt;guessing&lt;/em&gt; output length, which is SJF with the classic asterisk: SJF is provably optimal for mean waiting time and requires knowing job length, which you never do.&lt;/p&gt;

&lt;p&gt;The entire arc — FCFS is bad, preemption helps, priority needs aging or it starves, SJF needs an oracle — is the scheduling chapter. It's just running on hardware that costs $30,000 a card, which is why people are suddenly willing to fund the research.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Token-per-minute limits are a token bucket that can't price the job
&lt;/h2&gt;

&lt;p&gt;Last one, and this is where the analogy earns its keep by &lt;em&gt;breaking&lt;/em&gt; in an interesting place.&lt;/p&gt;

&lt;p&gt;LLM providers meter on two axes: requests per minute and &lt;strong&gt;tokens&lt;/strong&gt; per minute. RPM is the limiter you know. TPM is the one that actually binds, because a single 200k-context call can eat as much quota as fifty 4k-token calls — which is how you end up at 5% of your RPM, eating 429s all day.&lt;/p&gt;

&lt;p&gt;The algorithm underneath is the same token bucket that has been guarding APIs since forever. (The naming collision between "bucket tokens" and "LLM tokens" is the funniest accident in modern infrastructure and I refuse to stop enjoying it.) But a token bucket assumes &lt;strong&gt;cost is known at admission time&lt;/strong&gt;: a request arrives, it costs one token, you check, you decide. With an LLM call you know the input size and nothing else — the output length is decided by the model, one token at a time, over the next several seconds. You are being asked to admit a job to a fixed-capacity system without knowing what it will cost.&lt;/p&gt;

&lt;p&gt;Every strategy from there is a bet. Reserve pessimistically and you throttle yourself for capacity nobody used; debit as you stream and the limit becomes advisory, discovered only after you have blown it.&lt;/p&gt;

&lt;p&gt;I went down this exact rabbit hole in &lt;a href="https://subroute.dev/blog/llm-429-rate-limits-tokens-not-requests" rel="noopener noreferrer"&gt;Why your LLM app gets 429s even when you're under the rate limit&lt;/a&gt; — how each vendor accounts for it, the reserve-then-reconcile pattern, why your retry logic is making it worse, and what breaks once more than one worker shares the budget. For our purposes here the point is narrower, and it is the one thing on this list that your operating systems course genuinely did not prepare you for: &lt;strong&gt;admission control without a price tag.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the map actually tears
&lt;/h2&gt;

&lt;p&gt;I've spent 2,000 words arguing that this is all familiar, so let me be honest about the four places it genuinely isn't. These are the parts worth thinking hard about — and, not coincidentally, the parts that separate a good interview answer from a great one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allocations grow after they're made.&lt;/strong&gt; A &lt;code&gt;malloc&lt;/code&gt; gives you a size and that size is a fact. A KV cache allocation grows by one block every few steps for the entire life of the request, and it stops growing at a moment nobody can predict. Your allocator is servicing a workload where every live object is slowly inflating. There's no classic analogue to "the heap is fine right now but will be full in eleven seconds because of objects that already exist."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eviction cost isn't uniform.&lt;/strong&gt; LRU assumes a miss costs about the same regardless of which item you dropped. Evict a KV block and the cost of getting it back is proportional to how much prefix has to be recomputed — dropping a block from a 100k-token conversation is enormously more expensive than dropping one from a 500-token chat. The right policy has to weigh recompute cost, not just recency. (Weighted eviction exists in the classic literature, but it's the exception there and the default here.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One machine runs two opposite workloads.&lt;/strong&gt; Prefill is compute-bound: a big parallel matrix crunch over the whole prompt. Decode is memory-bandwidth-bound: one token at a time, dragging the entire weight matrix across the bus for each. They want different batch sizes, have different SLOs (time-to-first-token versus time-per-output-token), and they're contending for the same silicon. It's as if your database ran OLAP and OLTP on the same box with no isolation — which is why the newer designs disaggregate them onto separate pools entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost is a first-class design constraint, not an afterthought.&lt;/strong&gt; The reason interviewers grade cost explicitly now is that the arithmetic is brutal and unavoidable. Cache hit rates aren't a latency nicety here; a bad routing policy doesn't just make things slower, it multiplies your GPU bill. This is the part where "just add replicas" — the reflex that solves most web-tier problems — is the wrong answer, and being able to say why is the answer they're looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;The pattern I keep landing on is this: &lt;strong&gt;new hardware constraints don't invent new algorithms, they re-run the old tournament with different scoring.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Paging won on 1970s minicomputers because RAM was scarce and expensive. GPU memory is scarce and expensive, so paging won again. Cache affinity beats naive load balancing whenever recomputation is costly. Preemptive scheduling beats run-to-completion whenever job lengths vary wildly. These aren't facts about operating systems. They're facts about &lt;em&gt;resources under contention&lt;/em&gt;, and transformers didn't repeal them.&lt;/p&gt;

&lt;p&gt;Which means the way to get good at AI infrastructure is not to memorize this year's serving frameworks — those will churn, and half the specific numbers in this post will be stale within eighteen months. It's to get so fluent in the underlying dynamics that you recognize them on sight when they show up wearing a new name. Fragmentation looks like fragmentation. Head-of-line blocking looks like head-of-line blocking. A cache with the wrong routing policy looks like a cache with the wrong routing policy, whether it's holding rows or attention keys.&lt;/p&gt;

&lt;p&gt;That fluency is the thing I've been trying to build at &lt;a href="https://subroute.dev" rel="noopener noreferrer"&gt;Subroute&lt;/a&gt; — every algorithm is a live simulation you can run, tune, and break in the browser, because watching a policy fall over is worth more than reading about the failure mode. Right now it covers allocators, page replacement, cache eviction, rate limiting, load balancing, consistent hashing, and consensus, among others. Free, no signup.&lt;/p&gt;

&lt;p&gt;And if you've operated any of this at scale and something above doesn't match what you've seen in production, I want to hear it. The gap between the paper and the pager is where the interesting stuff lives.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://subroute.dev/blog/llm-serving-is-the-systems-you-already-know" rel="noopener noreferrer"&gt;subroute.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>llm</category>
      <category>inference</category>
      <category>operatingsystems</category>
    </item>
    <item>
      <title>I Got Tired of Watching YouTube Videos to Understand Graph Algorithms. So I Built This Instead.</title>
      <dc:creator>Yathiskumar</dc:creator>
      <pubDate>Wed, 10 Jun 2026 14:00:44 +0000</pubDate>
      <link>https://dev.to/yathiskumar/i-got-tired-of-watching-youtube-videos-to-understand-graph-algorithms-so-i-built-this-instead-2pif</link>
      <guid>https://dev.to/yathiskumar/i-got-tired-of-watching-youtube-videos-to-understand-graph-algorithms-so-i-built-this-instead-2pif</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5l7psgmufofw6l51i757.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5l7psgmufofw6l51i757.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Let me be honest with you.&lt;/p&gt;

&lt;p&gt;Graph algorithms always felt &lt;em&gt;odd&lt;/em&gt; to me.&lt;/p&gt;

&lt;p&gt;Every time I sat down to learn BFS or Dijkstra, the same thing happened. I'd read an article. Get confused. Open YouTube. Watch one video. Then another. Then a third one at 1.5x speed. Two hours later, I'd close my laptop thinking "okay, I kind of get it"... and forget everything by next week.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;The problem was never the videos. Some of them are great. The problem was that I was always &lt;em&gt;watching&lt;/em&gt; someone else's understanding. I was never &lt;em&gt;touching&lt;/em&gt; the algorithm myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I did something about it
&lt;/h2&gt;

&lt;p&gt;A while back, I started building &lt;strong&gt;Subroute&lt;/strong&gt; — an interactive playground for technical concepts. I started this journey with topics like rate limiting and caching.&lt;/p&gt;

&lt;p&gt;The idea is simple: &lt;strong&gt;learn by touching the system, not just reading about it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And now, the topic that troubled me the most is finally live — &lt;strong&gt;Graph Algorithms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 Check it out here: &lt;a href="https://subroute.dev/topics/graph-algorithms" rel="noopener noreferrer"&gt;subroute.dev/topics/graph-algorithms&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwakladgwrmeizpn15kh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwakladgwrmeizpn15kh.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside
&lt;/h2&gt;

&lt;p&gt;Ten algorithms. Each one with its own page, its own live prototype you can play with, and a small quiz to check yourself.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Breadth-First Search (BFS)&lt;/strong&gt; — visit a graph in expanding rings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth-First Search (DFS)&lt;/strong&gt; — go deep first, then back up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topological Sort&lt;/strong&gt; — the build-order and task-scheduling trick&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dijkstra&lt;/strong&gt; — the classic shortest path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bellman-Ford&lt;/strong&gt; — shortest path that survives negative edges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Floyd-Warshall&lt;/strong&gt; — every distance, between every pair&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Union-Find (DSU)&lt;/strong&gt; — connectivity in near-constant time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kruskal's MST&lt;/strong&gt; — cheapest network, sorted edges first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prim's MST&lt;/strong&gt; — cheapest network, grown from one node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A*&lt;/strong&gt; — the pathfinder behind games, GPS, and robots&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole track takes about 100 minutes. Not 100 minutes of watching. 100 minutes of &lt;em&gt;doing&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing that changed everything for me
&lt;/h2&gt;

&lt;p&gt;While building this, I realized something that no video ever told me clearly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't need to memorize ten algorithms. You only need to know which &lt;em&gt;question&lt;/em&gt; each one answers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What can I reach from here?" → BFS / DFS&lt;/li&gt;
&lt;li&gt;"What's the cheapest route?" → Dijkstra (or Bellman-Ford if weights can be negative)&lt;/li&gt;
&lt;li&gt;"How do I connect everything for the least cost?" → Kruskal / Prim&lt;/li&gt;
&lt;li&gt;"What's the fastest path to &lt;em&gt;that&lt;/em&gt; goal?" → A*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that clicked, the code became easy. Most of these are barely twenty lines. The hard part was never the code — it was knowing &lt;em&gt;when&lt;/em&gt; to use &lt;em&gt;what&lt;/em&gt;. So I added a side-by-side comparison table and a decision guide right on the page, because that's the cheat sheet I always wished I had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not "just another tutorial site"
&lt;/h2&gt;

&lt;p&gt;Because every concept here has a &lt;strong&gt;live prototype&lt;/strong&gt;. You don't read about how Dijkstra grows its frontier — you watch it happen, you poke it, you break it, you run it again.&lt;/p&gt;

&lt;p&gt;That's the whole point of Subroute. Reading gives you familiarity. Touching gives you intuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  I need your help
&lt;/h2&gt;

&lt;p&gt;I'm building this in public, and I genuinely want feedback. Brutal honesty welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the explanations make sense?&lt;/li&gt;
&lt;li&gt;Was any prototype confusing?&lt;/li&gt;
&lt;li&gt;Which topic should I build next?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go play with it: &lt;strong&gt;&lt;a href="https://subroute.dev/topics/graph-algorithms" rel="noopener noreferrer"&gt;subroute.dev/topics/graph-algorithms&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If even one person stops their endless YouTube loop and finally &lt;em&gt;gets&lt;/em&gt; graph algorithms because of this — building it was worth it.&lt;/p&gt;

&lt;p&gt;Drop your thoughts in the comments. I read every single one. 🙌&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>programming</category>
      <category>datastructures</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Three problems I had to solve to teach algorithms in a browser</title>
      <dc:creator>Yathiskumar</dc:creator>
      <pubDate>Mon, 25 May 2026 08:09:53 +0000</pubDate>
      <link>https://dev.to/yathiskumar/three-problems-i-had-to-solve-to-teach-algorithms-in-a-browser-171b</link>
      <guid>https://dev.to/yathiskumar/three-problems-i-had-to-solve-to-teach-algorithms-in-a-browser-171b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8f3c2u4i966mosaqr6s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8f3c2u4i966mosaqr6s.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few months ago, I noticed I was rereading the same ByteByteGo article on rate limiting for the fourth time.&lt;/p&gt;

&lt;p&gt;I could recite the token bucket definition. I could draw the diagram. I could explain it in an interview. But if you'd asked me &lt;em&gt;"what happens when the refill rate is half the request rate and the bucket is small?"&lt;/em&gt; — I'd have stared into the middle distance, run the math in my head, and given a noncommittal answer.&lt;/p&gt;

&lt;p&gt;The diagram wasn't the problem. The diagram was actually pretty good. The problem was that the diagram was &lt;em&gt;static&lt;/em&gt;, and the algorithm wasn't.&lt;/p&gt;

&lt;p&gt;Token buckets, sliding windows, leaky buckets, LRU caches, garbage collectors, load balancers — these are all systems that &lt;em&gt;do&lt;/em&gt; something over time. They have dynamics. Their interesting behavior is what happens at the edges: when load spikes, when memory fills, when one server dies. None of that lives in a diagram. It lives in &lt;em&gt;what happens next.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://subroute.dev" rel="noopener noreferrer"&gt;Subroute&lt;/a&gt; — a playground where every algorithm is a live simulation in the browser. You adjust the parameters, you watch it run, you break it. The goal is to skip the "stare at the diagram and imagine" step entirely.&lt;/p&gt;

&lt;p&gt;Building it taught me three things I didn't expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Time is a liar in the browser
&lt;/h2&gt;

&lt;p&gt;The first thing I learned is that you cannot trust the clock.&lt;/p&gt;

&lt;p&gt;A rate limiter is fundamentally about &lt;em&gt;time&lt;/em&gt;. Tokens refill at N per second. Windows tick over every X milliseconds. Requests arrive at some rate distribution. The whole concept depends on a consistent forward-marching clock.&lt;/p&gt;

&lt;p&gt;My first naive implementation used &lt;code&gt;setInterval(tick, 100)&lt;/code&gt;. Tick the simulation every 100ms, advance the algorithm, render the new state. It worked beautifully — until I tabbed away to check Slack.&lt;/p&gt;

&lt;p&gt;When you background a browser tab, most browsers throttle timers aggressively. &lt;code&gt;setInterval(tick, 100)&lt;/code&gt; becomes &lt;code&gt;setInterval(tick, 1000)&lt;/code&gt; or worse. Then I'd tab back, and the simulation would either freeze, lurch forward in a giant jump, or quietly desync — the visible state ahead of the algorithm's internal state by minutes of simulated time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Date.now()&lt;/code&gt; had the opposite problem. Real wall-clock time kept ticking, so if I used it as the source of truth, the bucket would silently "refill" 30 seconds worth of tokens the instant the tab regained focus. The simulation would jump, not freeze. Worse for teaching, because you couldn't see what had happened — you just saw the aftermath.&lt;/p&gt;

&lt;p&gt;The fix took two changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A virtual clock.&lt;/strong&gt; Every algorithm reads from &lt;code&gt;simulationTime&lt;/code&gt;, not &lt;code&gt;Date.now()&lt;/code&gt;. The simulation owns the concept of "now."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;requestAnimationFrame&lt;/code&gt; driver.&lt;/strong&gt; Instead of &lt;code&gt;setInterval&lt;/code&gt;, I increment &lt;code&gt;simulationTime&lt;/code&gt; by a controlled delta inside &lt;code&gt;rAF&lt;/code&gt;. When the tab backgrounds, &lt;code&gt;rAF&lt;/code&gt; pauses cleanly. When it foregrounds, it resumes from where it stopped. No drift, no surprise jumps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bonus, which I didn't see coming, is that once time is virtual it becomes &lt;em&gt;adjustable&lt;/em&gt;. I can give readers a speed slider — 0.25x to play through a slow burst in detail, 10x to fast-forward through a hundred refills in seconds. The same slider that solved a bug became one of the most useful teaching tools in the whole thing.&lt;/p&gt;

&lt;p&gt;That pattern kept showing up: &lt;strong&gt;the right primitive solves a bug and unlocks a feature you didn't plan for.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Randomness is the enemy of learning
&lt;/h2&gt;

&lt;p&gt;Once the clock was solid, I plugged in a random workload generator — Poisson arrivals, exponential intervals, the standard textbook stuff — and pointed it at five rate-limiting algorithms in parallel.&lt;/p&gt;

&lt;p&gt;They all looked identical.&lt;/p&gt;

&lt;p&gt;Token bucket, leaky bucket, fixed window, sliding window log, sliding window counter — five legitimately different algorithms with five different trade-offs. Under a uniformly random workload they were indistinguishable on the chart.&lt;/p&gt;

&lt;p&gt;Which makes sense in hindsight: averaged over enough randomness, &lt;em&gt;every&lt;/em&gt; rate limiter accepts the same fraction of requests. The differences only show up under &lt;em&gt;patterns&lt;/em&gt; — bursts, sustained pressure, mixed traffic.&lt;/p&gt;

&lt;p&gt;This is the gap between academic descriptions of algorithms and what they actually do in production. Real traffic isn't uniform random. Real traffic is Zipfian (a few keys dominate everything), bursty (most of the day is quiet, then 10x in a 30-second window), or scan-heavy (a backup job sweeps the entire keyspace once, blowing past every cache).&lt;/p&gt;

&lt;p&gt;I rewrote the workload generator with two changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Named presets instead of free parameters.&lt;/strong&gt; "Bursty," "scan," "Zipfian," "diurnal" — each one carefully constructed to make the differences between algorithms visible. The scan preset, for instance, is the single best demonstration of why ARC and LIRS exist and why vanilla LRU doesn't survive contact with production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seeded RNG.&lt;/strong&gt; Every preset uses a fixed seed by default, which means everyone who clicks "scan workload" sees the &lt;em&gt;exact same&lt;/em&gt; request stream. Reproducible. When someone tells me "the LRU panel looks broken at second 47," I can load the same seed and look at the same second.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The seeded-RNG decision turned out to matter for a reason I didn't foresee: it made the simulations &lt;em&gt;shareable&lt;/em&gt;. A reader can screenshot a moment and another reader can reproduce it. The simulation becomes a thing two people can have an argument about, which is what learning resources should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Side-by-side is a feature, not a layout
&lt;/h2&gt;

&lt;p&gt;The third decision was the one that changed the whole product.&lt;/p&gt;

&lt;p&gt;My early prototype showed one algorithm at a time. Pick "token bucket" from a dropdown, watch it run, switch to "leaky bucket," watch that one run separately. This is how every existing resource handles it — one algorithm per page, you flip between them.&lt;/p&gt;

&lt;p&gt;It doesn't work. By the time you've switched from "token bucket" to "leaky bucket" and watched it for ten seconds, you've already forgotten what the token bucket looked like. Comparison becomes a memory exercise.&lt;/p&gt;

&lt;p&gt;The change that fixed it sounds trivial: render all five algorithms simultaneously, side by side, on the same canvas. But the implementation matters — they had to share &lt;em&gt;one&lt;/em&gt; workload stream, not five independent ones. Otherwise you're back to comparing averages of randomness.&lt;/p&gt;

&lt;p&gt;The architecture became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One workload generator, producing a single stream of requests per simulation tick.&lt;/li&gt;
&lt;li&gt;Five algorithm panels, each subscribing to the same stream as a consumer.&lt;/li&gt;
&lt;li&gt;Each panel computes its own decision (accept/reject, hit/miss, route/queue) and renders its own state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is the most "aha" moment I've shipped. Hit the scan preset on the &lt;a href="https://subroute.dev/topics/cache-eviction" rel="noopener noreferrer"&gt;cache eviction page&lt;/a&gt; and watch four of the policies degrade in real time while ARC and LIRS hold their hit rate. You don't have to &lt;em&gt;explain&lt;/em&gt; scan resistance after that. The reader has seen it.&lt;/p&gt;

&lt;p&gt;This is the heuristic I now use to evaluate every new simulation: &lt;strong&gt;can a reader feel the difference between two algorithms in 30 seconds of playing?&lt;/strong&gt; If not, it's not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this taught me about teaching
&lt;/h2&gt;

&lt;p&gt;The pattern across all three problems was the same. Each one started as "how do I make this work technically" and ended as "this is what makes the algorithm legible."&lt;/p&gt;

&lt;p&gt;A virtual clock fixed a bug, then became the speed slider — and the speed slider is how readers see slow-motion bursts in detail.&lt;/p&gt;

&lt;p&gt;Seeded workloads fixed a reproducibility issue, then became the named presets — and the named presets are what reveal the algorithms' actual differences.&lt;/p&gt;

&lt;p&gt;Side-by-side rendering fixed a comparison problem, then became the core layout — and the core layout is what turns five separate articles into one playground.&lt;/p&gt;

&lt;p&gt;Reading about algorithms tells you what they are. Diagrams tell you what they look like. But intuition — the kind you need to make architecture decisions, debug latency spikes, or answer an interview follow-up — only comes from watching them &lt;em&gt;behave&lt;/em&gt;. The simulation is the teacher. The words around it are labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Subroute today covers six topics: rate limiting, cache eviction, cache write policies, garbage collection, memory allocation, load balancing. The next batch is on the runway: queues, consistent hashing, replication strategies, and the hard one — consensus.&lt;/p&gt;

&lt;p&gt;Raft and Paxos are where this approach has the most to prove. Most explanations of consensus are &lt;em&gt;very&lt;/em&gt; good at describing the happy path and &lt;em&gt;very&lt;/em&gt; bad at conveying what happens during a network partition. That's exactly where a simulation should win: slice the cluster in half, watch the leader election, scrub time backward, see exactly which node thought what when.&lt;/p&gt;

&lt;p&gt;If you want to poke the current set, &lt;a href="https://subroute.dev" rel="noopener noreferrer"&gt;subroute.dev&lt;/a&gt; is free and there's no signup. If you've built or operated any of these in production and something in a simulation feels off, I want to hear about it — that's the feedback that makes the next version better than the last.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post originally appeared on &lt;a href="https://subroute.dev/blog/building-interactive-system-design-simulations" rel="noopener noreferrer"&gt;subroute.dev/blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
