<?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: Vinayak Gaur</title>
    <description>The latest articles on DEV Community by Vinayak Gaur (@system_design_lab).</description>
    <link>https://dev.to/system_design_lab</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%2F4142594%2Fee062083-25e9-48c3-93df-07142e3cd1a8.png</url>
      <title>DEV Community: Vinayak Gaur</title>
      <link>https://dev.to/system_design_lab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/system_design_lab"/>
    <language>en</language>
    <item>
      <title>Why Cloudflare's New Stream Waits a Second on Purpose: Event Streams Explained</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Fri, 23 Oct 2026 14:15:09 +0000</pubDate>
      <link>https://dev.to/system_design_lab/why-cloudflares-new-stream-waits-a-second-on-purpose-event-streams-explained-14a</link>
      <guid>https://dev.to/system_design_lab/why-cloudflares-new-stream-waits-a-second-on-purpose-event-streams-explained-14a</guid>
      <description>&lt;p&gt;Sale night, 8 p.m. Meera's fraud checker crashes for four minutes, and her sneaker shop keeps taking orders the whole time. Every order in those four minutes was supposed to be checked for fraud. None of them were. The events were simply gone.&lt;/p&gt;

&lt;p&gt;Most engineers know the fix: put Kafka in the middle. But Cloudflare has machines in more than &lt;strong&gt;335 cities&lt;/strong&gt;, and it decided it couldn't run Kafka on them. So it built its own stream, called &lt;strong&gt;K2&lt;/strong&gt;, and K2 makes your writes wait. Up to a second. On purpose.&lt;/p&gt;

&lt;p&gt;This post walks through why. The animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1Wd0FVKkRpE" width="710" height="399"&gt;
  &lt;/iframe&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1c6kv257mg1yis3ud24.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1c6kv257mg1yis3ud24.jpg" alt="Slow, on purpose" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct calls drop events
&lt;/h2&gt;

&lt;p&gt;Follow one order: order 4471, one pair of sneakers. It has two readers. The fraud checker needs it now; the analytics job wants it for tonight's sales report.&lt;/p&gt;

&lt;p&gt;If the checkout calls each reader directly, that only works while both are up and both keep pace. On sale night, orders arrive faster than the fraud checker can work. Then it crashes, and there's nowhere for the event to wait, so it's dropped. Cloudflare's post names this exact trap: producers and consumers must keep up with each other, in volume and in time, or events get dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  A log in the middle
&lt;/h2&gt;

&lt;p&gt;The answer is a &lt;strong&gt;log&lt;/strong&gt;: an event stream that accepts every write, keeps it in order, and lets each reader catch up at its own pace. Order 4471 is appended to the end and gets a number, its &lt;strong&gt;offset&lt;/strong&gt;. The fraud checker remembers the last offset it read. If it crashes, the log keeps filling; when it comes back, it starts from its offset, and nothing is lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cloudflare couldn't run Kafka
&lt;/h2&gt;

&lt;p&gt;Kafka is the classic way to run that log. It stores it on the local disks of a cluster of servers called &lt;strong&gt;brokers&lt;/strong&gt;, and copies every piece to other brokers in case one dies. That works when brokers are big, long-lived machines on a reliable network.&lt;/p&gt;

&lt;p&gt;Cloudflare's edge is the opposite. Its machines are spread across 335+ cities, each job gets a small slice of one, the slice can disappear, and the cities often talk over the public internet. So where do you keep a log that must never lose an event, when no machine you run is permanent?&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the storage remember
&lt;/h2&gt;

&lt;p&gt;You don't keep it on your machines at all. Cloudflare already ran a service whose whole job is to never lose data: &lt;strong&gt;R2&lt;/strong&gt;, its object storage. Cloudflare says R2 is designed for eleven nines of durability, and it's strongly consistent: once a write succeeds, every reader sees it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqhgleh1fhqpzw1kx7r1.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqhgleh1fhqpzw1kx7r1.jpg" alt="The log lives in object storage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So K2 stores the log in R2. Copying data and agreeing on it, the hardest parts of a distributed log, become the storage layer's problem. K2 didn't solve replication. It handed it to a service that already had. The machines that handle events can come and go; the storage underneath doesn't care.&lt;/p&gt;

&lt;h2&gt;
  
  
  No append, so write whole files
&lt;/h2&gt;

&lt;p&gt;Object storage has one big gap: you can't append to an object. So K2 never appends. Meera's order lands on a Cloudflare edge service first, which holds it in a memory &lt;strong&gt;buffer&lt;/strong&gt;. The buffer waits a short moment for more orders, then writes everything it collected to R2 as one &lt;strong&gt;segment file&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70pt50bde7sqf2pl5yts.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70pt50bde7sqf2pl5yts.jpg" alt="Orders batched into one segment file" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each event still needs a strictly increasing offset. K2 gets that from R2's atomic operations, writes that happen all at once or not at all, so there's no separate coordination service to run. The checkout then hears back: accepted, or failed with a flag saying whether a retry is worth it.&lt;/p&gt;

&lt;p&gt;That short wait is the price. Cloudflare says it adds &lt;strong&gt;up to about one second at p99&lt;/strong&gt;: 99 writes in 100 come back faster than that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slower, but whose problem?
&lt;/h2&gt;

&lt;p&gt;Kafka writes to a broker's local disk in milliseconds; K2 waits for a batch, then a trip to object storage. But that second buys no cluster to run, no disks to fill, a bill that grows with the data you send, and cheap long history. For analytics, one second is nothing; for fraud, a second before an order ships is still fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queue or stream?
&lt;/h2&gt;

&lt;p&gt;Cloudflare also runs Queues, built for a different job: a &lt;strong&gt;queue&lt;/strong&gt; tracks, retries and dead-letters every message on its own, while a &lt;strong&gt;stream&lt;/strong&gt; moves batches for high volume, long retention and fan-out. A queue babysits every message. A stream moves the river.&lt;/p&gt;

&lt;h2&gt;
  
  
  Borrow a batch, don't own it
&lt;/h2&gt;

&lt;p&gt;K2 doesn't push events at readers; each reader asks. The fraud model calls consume for up to &lt;strong&gt;100 records&lt;/strong&gt; and gets a batch with a &lt;strong&gt;5-minute lease&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fien0yckezgc02or12sww.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fien0yckezgc02or12sww.jpg" alt="Consume, ack, nack, extend" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ack&lt;/strong&gt; when done, and the batch is never sent again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nack&lt;/strong&gt; if something went wrong, and it's redelivered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend&lt;/strong&gt; the lease if the work is slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replay sale night: the fraud model crashes holding order 4471. It never acknowledges, so once the lease is up another worker takes the batch. Same four-minute crash, and this time every order is still checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  One stream, many readers
&lt;/h2&gt;

&lt;p&gt;Readers are grouped by &lt;strong&gt;subscriptions&lt;/strong&gt;. Workers inside one subscription share the work, so when sale night gets busy, Meera adds three more fraud workers and they split the orders. Analytics gets a subscription of its own and sees every event at its own speed. In Cloudflare's example, the stream keeps events for seven days, so a reader that starts tomorrow can begin from the earliest one.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqr6bo75tkut5jrbj2fe.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqr6bo75tkut5jrbj2fe.jpg" alt="Two subscriptions, one stream" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Every read is billed
&lt;/h2&gt;

&lt;p&gt;K2 is free during the public beta on Workers Paid. After that, Cloudflare expects to charge &lt;strong&gt;$0.04 per GB produced&lt;/strong&gt;, &lt;strong&gt;$0.04 per GB consumed&lt;/strong&gt;, and &lt;strong&gt;$0.02 per GB-month&lt;/strong&gt; retained. With two subscriptions reading every byte, each gigabyte is paid for three times: once in, twice out. Adding workers splits the bill; adding a subscription adds a whole new consume charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kafka isn't dead, it's local
&lt;/h2&gt;

&lt;p&gt;Today K2 has no message keys, Kafka clients can't connect yet, and every reader polls. Push delivery, an express tier and multi-GB/s streams are on the roadmap.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiz7qnlipfe0xn2f281vi.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiz7qnlipfe0xn2f281vi.jpg" alt="The interview answer" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interview answer: Kafka keeps its log on local disks, fast but you run the cluster. A log on object storage lets the storage keep it safe, cheap and elastic but slower. If your events need answers in milliseconds, run the disks. If they need to survive anything, cheaply, and a second doesn't hurt, let the bucket remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A log in the middle lets producers and consumers stop keeping pace with each other.&lt;/li&gt;
&lt;li&gt;K2 hands replication to object storage and pays for it with up to ~1 s of write latency at p99.&lt;/li&gt;
&lt;li&gt;No append means batches: an edge buffer, then whole segment files.&lt;/li&gt;
&lt;li&gt;Leases with ack, nack and extend mean a crashed reader loses nothing.&lt;/li&gt;
&lt;li&gt;Fan-out is the stream's superpower, and on this price sheet, its fastest-growing line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch the full breakdown: &lt;a href="https://youtu.be/1Wd0FVKkRpE" rel="noopener noreferrer"&gt;https://youtu.be/1Wd0FVKkRpE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sources: Cloudflare, "Cloudflare K2: event streams on R2" (Micah Wylde, Marc Selwan), &lt;a href="https://blog.cloudflare.com/cloudflare-k2-streams/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/cloudflare-k2-streams/&lt;/a&gt; and InfoQ's coverage. Meera's shop and order 4471 are illustrative.&lt;/p&gt;

&lt;p&gt;More breakdowns like this: &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;https://systemdesignlab.in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>systemdesign</category>
      <category>cloudflare</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How Airbnb Taught Computers What a Home Feels Like</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:15:01 +0000</pubDate>
      <link>https://dev.to/system_design_lab/how-airbnb-taught-computers-what-a-home-feels-like-3p2m</link>
      <guid>https://dev.to/system_design_lab/how-airbnb-taught-computers-what-a-home-feels-like-3p2m</guid>
      <description>&lt;p&gt;Airbnb has millions of homes: treehouses, castles, city lofts, houseboats. When you look at one, Airbnb has to guess which others you'll like, not by price or city alone, but by &lt;strong&gt;feel&lt;/strong&gt;. Their answer was to turn every home into a list of just &lt;strong&gt;32 numbers&lt;/strong&gt;. Those numbers lifted clicks on their recommendations by &lt;strong&gt;21%&lt;/strong&gt;, and now help rank Airbnb's search results in real time.&lt;/p&gt;

&lt;p&gt;This post walks through how, from zero. The animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/-NK9hxCb4Sk" width="710" height="399"&gt;
  &lt;/iframe&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcouqhbqf7jy2ed6zldxy.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcouqhbqf7jy2ed6zldxy.jpg" alt="32 numbers per home, +21% clicks" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two doors to a booking
&lt;/h2&gt;

&lt;p&gt;Guests find the home they book through two doors. &lt;strong&gt;Search&lt;/strong&gt;: you type a city and dates, and a model ranks the results using more than a hundred signals. And the &lt;strong&gt;Similar Listings&lt;/strong&gt; carousel on every listing page. Together, search ranking and similar listings drive &lt;strong&gt;99%&lt;/strong&gt; of Airbnb's booking conversions.&lt;/p&gt;

&lt;p&gt;Both need the same thing: a way to say two homes are similar. Same city, same price, entire home versus private room: those are database columns. But guests also care about style, architecture and the feel of the place, and no column captures that. A cosy cabin and a glass penthouse can cost the same, sit in the same city, and appeal to completely different people.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an embedding is
&lt;/h2&gt;

&lt;p&gt;Imagine a map where every home is a dot, and homes that are alike sit close together. On a flat map each dot is two numbers, an x and a y. Now give the map &lt;strong&gt;32 directions&lt;/strong&gt; instead of two. Every home becomes a list of 32 numbers: its &lt;strong&gt;embedding&lt;/strong&gt;. You can't picture 32 dimensions, but you can still measure how close two homes are, with &lt;strong&gt;cosine similarity&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search sessions are sentences
&lt;/h2&gt;

&lt;p&gt;Nobody types those numbers in. They're &lt;strong&gt;learned&lt;/strong&gt;, with a trick borrowed from language models: words that keep showing up near each other tend to be related.&lt;/p&gt;

&lt;p&gt;Airbnb treats a guest's &lt;strong&gt;trail of clicks as a sentence&lt;/strong&gt;, and each listing ID as a word. A session is one uninterrupted run of clicks; a gap of more than &lt;strong&gt;30 minutes&lt;/strong&gt; starts a new one. When someone clicks three beach cottages in a row, that's a hint those cottages have something in common.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjlwcq98e6n96wnijtuw.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjlwcq98e6n96wnijtuw.jpg" alt="A trail of clicks is a sentence" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning by nudging
&lt;/h2&gt;

&lt;p&gt;Start every home at a random spot on the map. Slide a window along each session. The home in the middle is the central listing; the homes clicked just before and after it, up to five on each side, are its positive context: &lt;strong&gt;pull them closer&lt;/strong&gt;. Then grab a few homes completely at random and &lt;strong&gt;push them away&lt;/strong&gt;. That's &lt;strong&gt;negative sampling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Airbnb made two changes that fit its business:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The booking is the point.&lt;/strong&gt; Clicks are cheap; a booking is what the guest actually wanted. In sessions that end in a booking, the booked home stays in the context of every step. They call it &lt;strong&gt;global context&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same-city trap.&lt;/strong&gt; Guests search within one market, so positives come from one city, but random negatives come from the whole world. The model learns "Paris is not Tokyo" and never learns which Paris flats differ from each other. The fix: also sample negatives &lt;strong&gt;from the same market&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd4s3kga5qzqv1mmm88r.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd4s3kga5qzqv1mmm88r.jpg" alt="The same-city trap" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Training at Airbnb scale
&lt;/h2&gt;

&lt;p&gt;Airbnb trained this on more than &lt;strong&gt;800 million&lt;/strong&gt; search click sessions, producing an embedding for &lt;strong&gt;4.5 million&lt;/strong&gt; active listings. Why 32 numbers and not 300? Every vector has to live in the memory of the search machines for real-time comparisons, and 32 was the sweet spot between quality and RAM. A brand-new listing has no clicks yet, so it borrows: the average of the three geographically closest listings of the same type and price range.&lt;/p&gt;

&lt;p&gt;Nobody told the model about location, price or style, but it learned all three. California listings clustered by embedding land in real map areas. Same type and price score higher cosine similarity. And starting at a treehouse, its nearest neighbours are treehouses; castles find castles.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxecpz0z7kr9x4w4iiitp.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxecpz0z7kr9x4w4iiitp.jpg" alt="Learned from browsing, never labelled" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Similar Listings, rebuilt
&lt;/h2&gt;

&lt;p&gt;The carousel became pure geometry: take the home's vector, compare it with every home in the same market that's free on your dates, and keep the &lt;strong&gt;12&lt;/strong&gt; closest, a &lt;strong&gt;k-nearest-neighbours&lt;/strong&gt; search with k = 12. In an A/B test, the carousel's click-through rate rose &lt;strong&gt;21%&lt;/strong&gt;, and &lt;strong&gt;4.9%&lt;/strong&gt; more guests found the home they booked right there.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxxsr5mqt5cr3ae6bgf1.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxxsr5mqt5cr3ae6bgf1.jpg" alt="+21% clicks, +4.9% bookings found in the carousel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Search that learns while you browse
&lt;/h2&gt;

&lt;p&gt;For every user, Airbnb keeps two short lists, updated in real time with &lt;strong&gt;Kafka&lt;/strong&gt;: every listing you clicked in the last two weeks, and every listing you &lt;strong&gt;skipped&lt;/strong&gt; (ranked high, but you clicked something lower down). Each search gives every candidate two new scores, &lt;strong&gt;embedding click similarity&lt;/strong&gt; and &lt;strong&gt;embedding skip similarity&lt;/strong&gt;, both features in the ranking model. High click similarity pushes a home up; high skip similarity pushes it down. It launched in summer 2017.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Learn from behaviour, not labels.&lt;/li&gt;
&lt;li&gt;Shape the training around the outcome you care about: the booking.&lt;/li&gt;
&lt;li&gt;Keep vectors small enough to use in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same recipe works for almost any marketplace: products, songs, videos, jobs.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6nipocgebs7xc8588u5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6nipocgebs7xc8588u5.jpg" alt="The same recipe, any marketplace" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the full animated breakdown: &lt;a href="https://youtu.be/-NK9hxCb4Sk" rel="noopener noreferrer"&gt;https://youtu.be/-NK9hxCb4Sk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; Mihajlo Grbovic, Haibin Cheng, Qing Zhang, Lynn Yang, Phillippe Siclait and Matt Jones, "Listing Embeddings for Similar Listing Recommendations and Real-time Personalization in Search Ranking", The Airbnb Tech Blog (March 2018).&lt;/p&gt;

&lt;p&gt;Practice designing systems like this yourself at &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;https://systemdesignlab.in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>systemdesign</category>
      <category>recommendations</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How Uber Eats Halved Search Time With No Big Fix</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:15:01 +0000</pubDate>
      <link>https://dev.to/system_design_lab/how-uber-eats-halved-search-time-with-no-big-fix-4044</link>
      <guid>https://dev.to/system_design_lab/how-uber-eats-halved-search-time-with-no-big-fix-4044</guid>
      <description>&lt;p&gt;Uber Eats search had gotten so slow that Uber's own engineers called it &lt;strong&gt;untenable&lt;/strong&gt;. Then they cut the wait in half. The surprising part: no single change saved more than about &lt;strong&gt;200 milliseconds&lt;/strong&gt;. Some saved twenty.&lt;/p&gt;

&lt;p&gt;In September 2026, Uber Engineering published every change behind it. This post follows one search through the whole pipeline and pulls out the five ideas behind the fixes, ideas that work on any system you build. The animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/QlzyLS6ll24" width="710" height="399"&gt;
  &lt;/iframe&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fixxsu5vaaz3w71h58xhe.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fixxsu5vaaz3w71h58xhe.jpg" alt="The biggest single fix: about 200 ms" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight p.m., sushi: the six stages of a search
&lt;/h2&gt;

&lt;p&gt;Here's the search we'll follow. It's 8 p.m. Maya opens Uber Eats and types &lt;strong&gt;sushi&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Query understanding&lt;/strong&gt; works out what she means: sushi is a dish, not the name of a restaurant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt; searches a catalog of a few billion items for every restaurant and roll that could match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration&lt;/strong&gt; fetches the facts about each candidate: the signals a ranking model needs to judge it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking&lt;/strong&gt; scores the candidates so the best few rise to the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation&lt;/strong&gt; turns the winners into cards, with prices and photos.&lt;/li&gt;
&lt;li&gt;Finally, the cards travel back to &lt;strong&gt;Maya's phone&lt;/strong&gt; and draw on screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Six stages, one after another, and Maya waits through all of them.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8dfm66kypo8lyqgimbdx.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8dfm66kypo8lyqgimbdx.jpg" alt="The six stages, built up one at a time" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 1: measure what the user sees
&lt;/h2&gt;

&lt;p&gt;The usual answer to "how slow is it?" is back-end response time: how long the server takes to reply. Uber changed the clock. Their metric is &lt;strong&gt;above the fold&lt;/strong&gt;: from the moment Maya taps search to the moment the first screen of results is fully drawn, images included.&lt;/p&gt;

&lt;p&gt;That clock counts things the server timer never saw: sending results over the network, building the cards, loading the photos. And it pointed straight at the first fix. The server was building every result, one card at a time, before sending any of them.&lt;/p&gt;

&lt;p&gt;Now it sends a &lt;strong&gt;small first page&lt;/strong&gt; and keeps the rest &lt;strong&gt;cached on the server&lt;/strong&gt; for when Maya scrolls, and the cards &lt;strong&gt;render at the same time&lt;/strong&gt; instead of one by one. Together, those two changes made the first screen appear &lt;strong&gt;more than 200 ms sooner&lt;/strong&gt;, the biggest single win.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9dv9sjad2v2n8fw5du3.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9dv9sjad2v2n8fw5du3.jpg" alt="More than 200 ms, from measuring the right thing" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 2: delete work before you speed it up
&lt;/h2&gt;

&lt;p&gt;Retrieval was running several search strategies side by side and merging what they found. Some matched by meaning, some matched plain words. Uber checked what each one actually added. Several broad word-matching strategies were slow and contributed little the others hadn't already found. Removing them saved &lt;strong&gt;about 120 ms&lt;/strong&gt;, by deleting code. Duplicate chain stores are now removed earlier too, so every later stage carries fewer candidates.&lt;/p&gt;

&lt;p&gt;The catalog itself got the same treatment. Many of its few billion items are the same product sold by different stores, and every listing used to be its own item with its own data to fetch. Grouped by the product underneath, the catalog is &lt;strong&gt;roughly 100 times smaller&lt;/strong&gt;, and fetching data once per product cut lookups by &lt;strong&gt;more than 100 times&lt;/strong&gt;, about &lt;strong&gt;50 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 3: don't wait on what you don't need
&lt;/h2&gt;

&lt;p&gt;Hydration used to be one big step: fetch everything about every candidate, then rank. But ranking only needs the signals its model scores. The price of Maya's salmon roll, the promotion, whether it's in stock: those only matter when the card is drawn.&lt;/p&gt;

&lt;p&gt;So Uber split hydration in two. &lt;strong&gt;Ranking hydration&lt;/strong&gt; fetches only what scoring needs; &lt;strong&gt;presentation hydration&lt;/strong&gt; fetches the display details at the same time, in parallel. Ranking no longer waits for prices it never reads, which was worth &lt;strong&gt;more than 100 ms&lt;/strong&gt;. Removing false dependencies between steps, where one step waited on another it didn't actually need, saved &lt;strong&gt;about 35 ms&lt;/strong&gt; more.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjthvtvcz91clmd1lje6.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjthvtvcz91clmd1lje6.jpg" alt="Split hydration: more than 100 ms" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 4: hedge the tail
&lt;/h2&gt;

&lt;p&gt;Hydration also fans out: one search asks many machines at once, each holding one slice of the data, called a &lt;strong&gt;shard&lt;/strong&gt;. The answer is only as fast as the slowest shard. Most shards answer quickly, but now and then one stalls for a moment, and with enough shards per search, one of them is often stalling. That's &lt;strong&gt;tail latency&lt;/strong&gt;: a rare slow case, multiplied by fan-out, becomes the common case.&lt;/p&gt;

&lt;p&gt;Uber's fix is &lt;strong&gt;request hedging&lt;/strong&gt;. If a shard hasn't answered within a time limit, send the same request to another copy and take whichever answer comes back first. It works because the stalls were isolated, one shard at a time, and it saved &lt;strong&gt;40 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's safe here because hydration only reads. Send a payment twice and you charge twice: writes need idempotency keys.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9spkvqmyfmn6towyu62r.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9spkvqmyfmn6towyu62r.jpg" alt="Request hedging: ask twice, take the faster" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 5: lay data out the way it's read
&lt;/h2&gt;

&lt;p&gt;Every search runs a small ad auction with &lt;strong&gt;hundreds of bids&lt;/strong&gt; to score. The bids were stored row by row, with the same shared details repeated in every row. But scoring reads one field across every bid, like running down one column of a spreadsheet, so Uber stored the bids &lt;strong&gt;as columns&lt;/strong&gt; and moved campaign and pacing data into the ad service's own memory. Ad selection got &lt;strong&gt;about 30 ms&lt;/strong&gt; faster, bid preparation &lt;strong&gt;about 110 ms&lt;/strong&gt;, and cutting needless format conversions saved &lt;strong&gt;20 ms&lt;/strong&gt; more.&lt;/p&gt;

&lt;p&gt;The plumbing had the same lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Results encoded one after another left most CPU cores idle; encoding in parallel saved &lt;strong&gt;more than 50 ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Embeddings rounded to five decimal places and packed compactly shrank &lt;strong&gt;46%&lt;/strong&gt;, and their database queries got twice as fast.&lt;/li&gt;
&lt;li&gt;The service mesh capped requests per connection, so busy services queued silently; parallel connections removed &lt;strong&gt;up to 53 ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In some Go services, garbage collection was eating &lt;strong&gt;more than 40% of the CPU&lt;/strong&gt;. Plain values instead of pointers let data live on the stack and disappear when a function returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uber even built an AI agent loop that profiles live latency, drafts fixes, opens pull requests and benchmarks them. In Uber's words, what it finds is "individually uninspiring but compound meaningfully at scale."&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Measure what the user sees, not what your server times.&lt;/li&gt;
&lt;li&gt;Delete work before you make it faster.&lt;/li&gt;
&lt;li&gt;Draw your request's dependency graph; anything that doesn't need to wait, shouldn't.&lt;/li&gt;
&lt;li&gt;Hedge the tail on reads; never on payments.&lt;/li&gt;
&lt;li&gt;Lay data out the way it's read, and keep the hot part in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those fixes was big on its own. Together, Maya's sushi shows up in half the time.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jw0orlma1tdiy5i1114.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jw0orlma1tdiy5i1114.jpg" alt="The five ideas and where the time went" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the full animated breakdown: &lt;a href="https://youtu.be/QlzyLS6ll24" rel="noopener noreferrer"&gt;https://youtu.be/QlzyLS6ll24&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; Uber Engineering, "Halving the Time: How Uber Eats Rebuilt Its Search Pipeline" (Sept 2026), &lt;a href="https://www.uber.com/us/en/blog/uber-eats-search-pipeline/" rel="noopener noreferrer"&gt;https://www.uber.com/us/en/blog/uber-eats-search-pipeline/&lt;/a&gt; · InfoQ, &lt;a href="https://www.infoq.com/news/2026/10/uber-eats-search-latency/" rel="noopener noreferrer"&gt;https://www.infoq.com/news/2026/10/uber-eats-search-latency/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Practice designing systems like this yourself at &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;https://systemdesignlab.in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>performance</category>
      <category>distributedsystems</category>
      <category>go</category>
    </item>
    <item>
      <title>How 5 Servers Agree on One Truth: Raft Consensus Explained</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:15:01 +0000</pubDate>
      <link>https://dev.to/system_design_lab/how-5-servers-agree-on-one-truth-raft-consensus-explained-4b1f</link>
      <guid>https://dev.to/system_design_lab/how-5-servers-agree-on-one-truth-raft-consensus-explained-4b1f</guid>
      <description>&lt;p&gt;The server in charge just crashed. In the Raft authors' own tests, with fixed election timers, the other four servers argued for &lt;strong&gt;more than ten seconds&lt;/strong&gt; about who should take over. Then they added &lt;strong&gt;five milliseconds of randomness&lt;/strong&gt;, and the median wait dropped to &lt;strong&gt;287 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Raft is the algorithm that lets a group of machines agree on one truth, even when two of five of them die. It runs inside etcd, the key-value store Kubernetes keeps its cluster data in. This post walks through how it works, following one example the whole way. The animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/J_qzUMR3JbA" width="710" height="399"&gt;
  &lt;/iframe&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59ib7lhck65sjbuig6gm.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59ib7lhck65sjbuig6gm.jpg" alt="Raft: one truth, five servers" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One copy isn't enough
&lt;/h2&gt;

&lt;p&gt;A ticket shop is selling the last seat of a sold-out concert: &lt;strong&gt;seat 7A&lt;/strong&gt;. If the shop keeps its bookings on one server, life is simple, until that server dies and the shop closes.&lt;/p&gt;

&lt;p&gt;So you keep copies on several servers. Now any one of them can die and the others carry on. But copies bring a new problem: Priya books seat 7A on one server, and a second later Leo books the same seat on another. Both servers said yes. The seat is sold twice. The copies have to agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same commands, same order
&lt;/h2&gt;

&lt;p&gt;Raft's answer starts with a &lt;strong&gt;log&lt;/strong&gt;. Every change is written down as a numbered command, like "seat 7A goes to Priya". Each server keeps its own copy of the log and plays the commands into its own copy of the data, one by one. If every server has the same commands in the same order, every server ends up in exactly the same state. The paper calls this a &lt;strong&gt;replicated state machine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The whole job of consensus is to keep those logs identical while servers crash, restart and lose messages. The trick is &lt;strong&gt;majorities&lt;/strong&gt;: a change only counts once more than half the servers have it. With five servers, that's three, so five servers can lose any two and keep working. That's why five is the typical size.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenz0x7zdoxkfudl0wdq3.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenz0x7zdoxkfudl0wdq3.jpg" alt="Three of five is a majority" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who's in charge
&lt;/h2&gt;

&lt;p&gt;Raft elects &lt;strong&gt;one leader&lt;/strong&gt;. Every write goes through it, and log entries only flow from the leader to the others. Each server is a &lt;strong&gt;leader&lt;/strong&gt;, a &lt;strong&gt;follower&lt;/strong&gt; or a &lt;strong&gt;candidate&lt;/strong&gt;. Followers are passive: they answer, unless the leader goes quiet.&lt;/p&gt;

&lt;p&gt;The leader sends a steady &lt;strong&gt;heartbeat&lt;/strong&gt; to every follower. Time is split into numbered &lt;strong&gt;terms&lt;/strong&gt;, each starting with an election and having at most one leader. A server that sees a higher term catches up at once, and an old leader that sees one steps down.&lt;/p&gt;

&lt;h2&gt;
  
  
  An election
&lt;/h2&gt;

&lt;p&gt;Every follower runs an &lt;strong&gt;election timer&lt;/strong&gt;, reset by each heartbeat. When server one, the leader for term 4, dies, the heartbeats stop. Server three's timer runs out first. It moves to term 5, becomes a candidate, votes for itself and asks everyone else for their vote.&lt;/p&gt;

&lt;p&gt;Each server gets &lt;strong&gt;one vote per term&lt;/strong&gt;, first come, first served. Servers two and four say yes: three of five, a majority. Server three leads term 5 and starts sending heartbeats. Because every server votes once per term and a win needs three votes, two leaders can never win the same term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split votes and the random timer
&lt;/h2&gt;

&lt;p&gt;If two servers time out at the same moment, they can split the vote and nobody reaches three. With identical timers, they collide again and again. That's why fixed timeouts took over ten seconds. The fix: each server picks its timeout &lt;strong&gt;at random&lt;/strong&gt;, from a range like 150–300 ms. One server almost always wakes first and wins before anyone else wakes up.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2xoq1mhhtif8bxlm9k9q.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2xoq1mhhtif8bxlm9k9q.jpg" alt="A random timeout breaks the tie" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The timeout needs to be much longer than one round of messages (about 0.5 to 20 ms) and much shorter than the months between server failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Committing a write
&lt;/h2&gt;

&lt;p&gt;Priya taps buy. Her request goes to the leader (a follower would just redirect her). The leader appends &lt;strong&gt;index 7, term 5: 7A → Priya&lt;/strong&gt; to its log and sends it to all four followers at once in an &lt;strong&gt;AppendEntries&lt;/strong&gt; message. The same message with no entries is the heartbeat.&lt;/p&gt;

&lt;p&gt;Servers two and four write it and say yes. With the leader's own copy, that's three of five, so the entry is &lt;strong&gt;committed&lt;/strong&gt;: it survives even if the leader dies a second later. Priya sees her ticket. A slow server five holds nobody up; the leader keeps retrying until it catches up.&lt;/p&gt;

&lt;h2&gt;
  
  
  When logs disagree
&lt;/h2&gt;

&lt;p&gt;Crashes leave messes: missing entries, or extra entries that were never committed. So every AppendEntries also carries the &lt;strong&gt;index and term of the entry just before&lt;/strong&gt; the new ones. If the follower doesn't have that exact entry, it refuses. The leader steps back one entry and tries again until the logs agree, and the follower replaces everything after that point with the leader's entries. The leader never changes its own log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not just anyone can lead
&lt;/h2&gt;

&lt;p&gt;Imagine the leader crashed the instant Priya's booking committed, and server five, which never got it, asks for votes. If it won, Priya's booking would be wiped out. So Raft adds one rule: &lt;strong&gt;a server refuses its vote if its own log is more up to date than the candidate's.&lt;/strong&gt; Compare the last entries: the later term wins; if the terms match, the longer log wins.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkq1kgllog9rbouz2abrp.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkq1kgllog9rbouz2abrp.jpg" alt="Two groups of three always share a server" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Priya's entry is on three servers, and any candidate needs three votes. Two groups of three out of five always share at least one server, and that server refuses to vote for a log without the booking. So every new leader already has every committed entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leader that didn't know
&lt;/h2&gt;

&lt;p&gt;If the network splits with the leader on the small side, the big side elects a new leader for term 6. For a moment two servers think they're leader, but the old one can only reach two of five, so nothing it writes commits. When the network heals, it sees term 6 and steps down, and its uncommitted entry is overwritten. &lt;strong&gt;Bad timing can slow Raft down, but it can't make two servers disagree.&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93rw2e8lzfpw5xorr1bf.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93rw2e8lzfpw5xorr1bf.jpg" alt="Only one side can commit" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and where it runs
&lt;/h2&gt;

&lt;p&gt;Every write waits for a round trip to a majority, and each follower writes the entry to disk before it answers. Every write goes through one leader. Six servers survive no more failures than five. If three of five go down, the cluster stops taking writes rather than risk two different answers. And Raft assumes servers crash or go silent; it doesn't defend against a server that lies on purpose.&lt;/p&gt;

&lt;p&gt;Raft was designed to be understandable: in a study of 43 students who learned both, 33 scored better on Raft than on Paxos. That clarity helped it spread: etcd, HashiCorp's Consul and TiKV all run on it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4xrku5qpjsq7ziwp17k.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4xrku5qpjsq7ziwp17k.jpg" alt="Elect, commit, only up-to-date leaders" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Same commands in the same order give the same state on every server.&lt;/li&gt;
&lt;li&gt;A write counts once a majority holds it: 3 of 5 survives two failures; use odd cluster sizes.&lt;/li&gt;
&lt;li&gt;Elect one leader on a random timer; one vote per server per term.&lt;/li&gt;
&lt;li&gt;Only an up-to-date server can win, so every leader has every committed entry.&lt;/li&gt;
&lt;li&gt;Safety never depends on timing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch the full animated breakdown: &lt;a href="https://youtu.be/J_qzUMR3JbA" rel="noopener noreferrer"&gt;https://youtu.be/J_qzUMR3JbA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; Diego Ongaro and John Ousterhout, &lt;em&gt;In Search of an Understandable Consensus Algorithm (Extended Version)&lt;/em&gt;, &lt;a href="https://raft.github.io/raft.pdf" rel="noopener noreferrer"&gt;https://raft.github.io/raft.pdf&lt;/a&gt; · &lt;a href="https://raft.github.io/" rel="noopener noreferrer"&gt;https://raft.github.io/&lt;/a&gt; · etcd FAQ · Kubernetes architecture docs · HashiCorp Consul consensus docs.&lt;/p&gt;

&lt;p&gt;Practice designing systems like this yourself at &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;https://systemdesignlab.in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>database</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How LinkedIn Rebuilt Its Feed: FollowFeed Explained</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:49:59 +0000</pubDate>
      <link>https://dev.to/system_design_lab/how-linkedin-rebuilt-its-feed-followfeed-explained-4ok9</link>
      <guid>https://dev.to/system_design_lab/how-linkedin-rebuilt-its-feed-followfeed-explained-4ok9</guid>
      <description>&lt;p&gt;Every time you open LinkedIn, your feed is built from scratch. For &lt;strong&gt;400 million members&lt;/strong&gt;, in about &lt;strong&gt;a tenth of a second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For years that feed ran on a search engine built for something else, until the cost stopped making sense. A team spent &lt;strong&gt;twenty months&lt;/strong&gt; replacing it. The new system, FollowFeed, is &lt;strong&gt;five times faster&lt;/strong&gt;, holds &lt;strong&gt;twenty times more data&lt;/strong&gt;, and runs on &lt;strong&gt;half the servers&lt;/strong&gt;. The trick behind all three numbers is that your feed is never stored at all. The animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/WlEdcWpXn7M" width="710" height="399"&gt;
  &lt;/iframe&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4u8ew1ehb7c2df8xd924.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4u8ew1ehb7c2df8xd924.jpg" alt="A feed assembled per request" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A feed is a question
&lt;/h2&gt;

&lt;p&gt;You follow people and companies. Each one produces content: shares, likes, comments. Call each of those streams a &lt;strong&gt;timeline&lt;/strong&gt;: a list of what that entity did, newest first.&lt;/p&gt;

&lt;p&gt;Your feed isn't a thing stored somewhere. It's an answer to a question: &lt;em&gt;out of every timeline I follow, what are the best few dozen items right now?&lt;/em&gt; LinkedIn's index holds hundreds of millions of timelines. Answering that question, fast, is the whole problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system they outgrew
&lt;/h2&gt;

&lt;p&gt;The old feed ran on &lt;strong&gt;Sensei&lt;/strong&gt;, a generic search platform built on Lucene. Lucene's core trick is the inverted index: a map from every term to the documents containing it. It's fast if you keep it in memory, and Sensei did. That's the catch.&lt;/p&gt;

&lt;p&gt;More content meant more memory, which meant more machines, and membership doubled in three years, from 200 million to 400 million. Sensei was also three projects stitched together (Bobo, Norbert and Zoie), and once LinkedIn built a new search engine, Galene, Sensei stopped being anyone's priority. The maintenance bill stayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push or pull
&lt;/h2&gt;

&lt;p&gt;When someone you follow posts, do you &lt;strong&gt;push&lt;/strong&gt; it or &lt;strong&gt;pull&lt;/strong&gt; it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push (fan-out-on-write):&lt;/strong&gt; copy the post into the pre-built feed of every follower, immediately. Reads are trivially cheap, because your feed is already sitting there, finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull (fan-out-on-read):&lt;/strong&gt; store the post once, in its author's timeline, and assemble the feed when someone asks.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0k9h95scl6m1wwabztrs.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0k9h95scl6m1wwabztrs.jpg" alt="Push stores 62 times more data" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Push is fine until you count the copies: LinkedIn measured &lt;strong&gt;62 times more data&lt;/strong&gt; than the pull design. And a pre-built feed is a frozen ranking. Change the relevance model and you have to re-rank everyone's stored feed.&lt;/p&gt;

&lt;p&gt;So FollowFeed pulls. Feeds are built at query time, every time.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facnoqclhhng0mkytzmab.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Facnoqclhhng0mkytzmab.jpg" alt="Built at query time" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the code to the data
&lt;/h2&gt;

&lt;p&gt;Pulling has a cost: the work now happens while the member is waiting. Personalizing a feed isn't one lookup. The model wants thousands of candidate records, and the naive version ships all of them across the network to a scoring service. The network is the slow part.&lt;/p&gt;

&lt;p&gt;So FollowFeed inverts it. Scoring runs &lt;strong&gt;on the index nodes&lt;/strong&gt;, the machines already holding the data. Each node filters and scores its own records, and returns only the handful that survived.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpenhywrx11zpit681h0e.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpenhywrx11zpit681h0e.jpg" alt="Move the computation to the data" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Timelines on disk
&lt;/h2&gt;

&lt;p&gt;If you're not holding everything in memory, you need storage that's fast on disk. FollowFeed uses &lt;strong&gt;RocksDB&lt;/strong&gt;, an embeddable key-value store built on LevelDB and tuned for SSDs. LinkedIn runs Java and RocksDB is C++, so the team wrote the bindings and contributed them back to open source.&lt;/p&gt;

&lt;p&gt;A timeline isn't stored as one key per post (that would be one disk lookup per post). It's a &lt;strong&gt;linked list of blobs&lt;/strong&gt;. Each blob holds a chunk of serialized records plus metadata: the timestamp range it covers and how many records are inside. Records use Avro, so each node keeps a schema registry inside RocksDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven hundred and twenty
&lt;/h2&gt;

&lt;p&gt;Hundreds of millions of timelines don't fit on one machine. The naive choice is one partition per node, but then adding a node means splitting partitions and moving the pieces.&lt;/p&gt;

&lt;p&gt;FollowFeed over-partitions instead: the data is cut into &lt;strong&gt;720 partitions&lt;/strong&gt;, permanently. The number is chosen for its divisors. 720 divides evenly by 40, 45, 60, 80 and 360, so the cluster can be any of those sizes and every node still gets whole partitions. You move partitions; you never split one. Rebalancing becomes bookkeeping instead of surgery.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm3hq9r1jfln6etrtko5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm3hq9r1jfln6etrtko5.jpg" alt="720 partitions, fixed for good" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two caches
&lt;/h2&gt;

&lt;p&gt;There's a cache in front of RocksDB, and its shape is the interesting part. Some timeline keys map to long lists of records, others to nothing at all. Size a cache by entry count, and a few huge entries blow the memory budget.&lt;/p&gt;

&lt;p&gt;So FollowFeed runs two: a &lt;strong&gt;fat cache&lt;/strong&gt; for keys with records, measured in total records (its weight), and a &lt;strong&gt;skinny cache&lt;/strong&gt; for keys with no records, capped by key count. Both are Guava caches, split into sub-caches so threads don't queue on one lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  One request, end to end
&lt;/h2&gt;

&lt;p&gt;A request arrives at a &lt;strong&gt;broker&lt;/strong&gt; with the entities you follow, your filter criteria and how many records to return. The broker finds the right index nodes through &lt;strong&gt;D2&lt;/strong&gt;, LinkedIn's name service and load balancer (backed by ZooKeeper), and fans the query out in parallel. Each node pulls candidates from cache or RocksDB, filters them with a small SQL-like grammar (privacy, geography, content type), scores and ranks what's left, and returns its best records. The broker merges, drops duplicates, applies diversity filtering, and returns the top few.&lt;/p&gt;

&lt;p&gt;Scoring is where the latency budget goes. The team started with a general-purpose scoring library that interprets the model at runtime, then &lt;strong&gt;generated Java code&lt;/strong&gt; for the model instead: &lt;strong&gt;50 microseconds per record at p99&lt;/strong&gt;, fifteen times faster than where they started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it fed, everywhere
&lt;/h2&gt;

&lt;p&gt;Everything that happens on LinkedIn lands on a Kafka stream, but that stream is partitioned by Kafka's rules, not FollowFeed's 720. A service in the middle, the &lt;strong&gt;Partitioner&lt;/strong&gt;, republishes the firehose onto topics matching the index nodes' partition ranges. Adding a replica means starting a node and pointing it at the same topics. Every timeline is replicated to every datacenter, and each datacenter's serving cluster is sized to the read traffic it actually takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast at the tail
&lt;/h2&gt;

&lt;p&gt;Broker calls are asynchronous, so one slow node can't hold the others hostage, and if a node is slow the broker fires the same request at up to three replicas and takes the first answer. Inside each node: read-copy-update, fine-grained locking, and tuned garbage collection, caches and networking.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshb0m659adcappb4gpca.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshb0m659adcappb4gpca.jpg" alt="The results" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it bought them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Mobile feed queries at p99: about &lt;strong&gt;140 ms&lt;/strong&gt;, five times faster than Sensei&lt;/li&gt;
&lt;li&gt;Page load at p90: &lt;strong&gt;150 ms&lt;/strong&gt; better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20 times more data&lt;/strong&gt; in the index, on &lt;strong&gt;half the hardware&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A feed can be a &lt;strong&gt;query&lt;/strong&gt;, not a table. Pull avoids 62× duplication and frozen rankings.&lt;/li&gt;
&lt;li&gt;When the network is the bottleneck, &lt;strong&gt;move the computation to the data&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-partition&lt;/strong&gt; with a number that has many divisors, so rebalancing never splits a partition.&lt;/li&gt;
&lt;li&gt;Size caches by what actually costs memory, not by entry count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch the full breakdown: &lt;a href="https://youtu.be/WlEdcWpXn7M" rel="noopener noreferrer"&gt;https://youtu.be/WlEdcWpXn7M&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/blog/engineering/feed/followfeed-linkedin-s-feed-made-faster-and-smarter" rel="noopener noreferrer"&gt;FollowFeed: LinkedIn's Feed Made Faster and Smarter&lt;/a&gt;, LinkedIn Engineering.&lt;/p&gt;

&lt;p&gt;Practice designing systems like this yourself at &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;systemdesignlab.in&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why Your Payment Got Charged Twice: Idempotency Keys Explained</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:15:06 +0000</pubDate>
      <link>https://dev.to/system_design_lab/why-your-payment-got-charged-twice-idempotency-keys-explained-2kcm</link>
      <guid>https://dev.to/system_design_lab/why-your-payment-got-charged-twice-idempotency-keys-explained-2kcm</guid>
      <description>&lt;p&gt;Maya taps &lt;strong&gt;Pay&lt;/strong&gt; for a $20 ride. Ten seconds later: request timed out. The app retries, and Maya gets charged &lt;strong&gt;$40&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The retry went through. So did the first attempt. The first request never failed: it worked perfectly, and the answer just never made it back.&lt;/p&gt;

&lt;p&gt;Stripe's fix for this is one extra header on the request, a random string called an &lt;strong&gt;idempotency key&lt;/strong&gt;. This post walks through how it works and how to build it yourself. The animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/OE_RnizyyEM" width="710" height="399"&gt;
  &lt;/iframe&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki0gku2dtissm091klpi.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki0gku2dtissm091klpi.jpg" alt="Two charges, for one ride" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to fail
&lt;/h2&gt;

&lt;p&gt;Stripe's engineers start from a blunt fact: the network is unreliable. A call from a client to a server can fail in three places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Before the request reaches the server.&lt;/strong&gt; Nothing happened, so a retry is safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halfway through.&lt;/strong&gt; The server started working and something broke. Maybe the charge ran, maybe it didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After the work is done.&lt;/strong&gt; The money moved, then the connection dropped before the response got back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the client's side, all three look exactly the same: a timeout and no answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry, or give up?
&lt;/h2&gt;

&lt;p&gt;Both obvious options are wrong. &lt;strong&gt;Never retry&lt;/strong&gt;, and case 1 gives Maya a free ride. &lt;strong&gt;Always retry&lt;/strong&gt;, and case 3 charges her twice. Waiting longer doesn't help you choose: a slow server and a dead connection look identical from the outside.&lt;/p&gt;

&lt;p&gt;What you want is a retry that's safe no matter which case happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "idempotent" means
&lt;/h2&gt;

&lt;p&gt;An operation is &lt;strong&gt;idempotent&lt;/strong&gt; if doing it twice has the same effect as doing it once. A &lt;code&gt;GET&lt;/code&gt; only reads. &lt;code&gt;DELETE&lt;/code&gt; the same thing twice and it's just as deleted. Stripe's article uses a &lt;code&gt;PUT&lt;/code&gt; to a DNS provider: "set this record to this value" can be sent twice and the record ends up the same.&lt;/p&gt;

&lt;p&gt;A charge isn't like that. A &lt;code&gt;POST /v1/charges&lt;/code&gt; with &lt;code&gt;amount=2000&lt;/code&gt; (that's $20 in cents) says &lt;em&gt;make a new charge&lt;/em&gt;, not &lt;em&gt;which&lt;/em&gt; charge. The server can't tell a retry from a new purchase. Send it twice and you get two charge objects and $40 gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idempotency key
&lt;/h2&gt;

&lt;p&gt;The server needs a name for the attempt. That's the idempotency key: the client makes up a unique string and sends it in an &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhj3169vmbp38q1a00i68.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhj3169vmbp38q1a00i68.jpg" alt="One header names the attempt" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe suggests a &lt;strong&gt;V4 UUID&lt;/strong&gt;, or any string random enough that two clients never pick the same one. Keys can be up to &lt;strong&gt;255 characters&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The crucial part: the key is made &lt;strong&gt;once, before the first attempt&lt;/strong&gt;, and every retry of that payment reuses it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of a coat check: show ticket 42 twice and you get the same coat back, not a second coat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server remembers
&lt;/h2&gt;

&lt;p&gt;Each key points to a saved result. Stripe saves the &lt;strong&gt;first response&lt;/strong&gt; for every key: its status code and body.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First time a key arrives: no entry, so the server runs the charge and stores the response against the key.&lt;/li&gt;
&lt;li&gt;Second time: the entry is there, so it charges nothing and returns the saved response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It saves the result whether the request succeeded or failed. If the first attempt returned a 500, the retry gets that same 500. A key doesn't mean "try again from scratch". It means "tell me what happened to this request".&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard case: crashing halfway
&lt;/h2&gt;

&lt;p&gt;Replay the three failures with a key. Case 1: the server never saw the key, so the retry runs normally. Case 3: the retry finds the key and gets the saved reply. Case 2 is the hard one: the server died partway through, so it has to remember how far it got.&lt;/p&gt;

&lt;p&gt;Brandur Leach, who wrote Stripe's article, sketches a Postgres design where each key's row stores a &lt;strong&gt;recovery point&lt;/strong&gt;: &lt;code&gt;started&lt;/code&gt;, &lt;code&gt;ride_created&lt;/code&gt;, &lt;code&gt;charge_created&lt;/code&gt;, &lt;code&gt;finished&lt;/code&gt;. A retry picks up from there, and each step between calls to other services commits all at once or not at all.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmusagp5bv4xp03ybkki.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmusagp5bv4xp03ybkki.jpg" alt="Recovery points in the key's row" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When his server calls Stripe, it passes its own idempotency key along, so even a crash in the middle of the charge can't charge Maya twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases that bite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Two retries at once.&lt;/strong&gt; Both look up the key and find no finished result. So the key's row gets a lock: the first request stamps a &lt;code&gt;locked_at&lt;/code&gt; time and does the work, and the second gets a &lt;strong&gt;409 Conflict&lt;/strong&gt;. Nothing is saved or charged, and the client retries later.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7be7h4v2kbvg74vt5aqt.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7be7h4v2kbvg74vt5aqt.jpg" alt="409 Conflict for the concurrent retry" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same key, different request.&lt;/strong&gt; If a bug reuses Maya's key for a $50 charge, Stripe compares it with the original and rejects it. Keys are also scoped per user, and should never be built from personal data like an email address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long to remember.&lt;/strong&gt; Stripe lets keys be pruned once they're at least &lt;strong&gt;24 hours&lt;/strong&gt; old. For your own system Brandur suggests about &lt;strong&gt;72 hours&lt;/strong&gt;, so a bad Friday deploy still has its keys through the weekend. A background "reaper" deletes old keys, and a "completer" pushes abandoned requests to the finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back off, and add jitter
&lt;/h2&gt;

&lt;p&gt;Keys make a retry &lt;em&gt;safe&lt;/em&gt;. They don't make it &lt;em&gt;polite&lt;/em&gt;. If the server goes down and thousands of clients retry instantly, they hammer the thing that needs to recover.&lt;/p&gt;

&lt;p&gt;Stripe's answer is &lt;strong&gt;exponential backoff&lt;/strong&gt;: wait 2^n, where n is the number of failures so far. But if an outage hits thousands of clients in the same second, they all back off on the same schedule and come back in synchronized waves: the &lt;strong&gt;thundering herd&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mgrlnntg6o7kdvaklqu.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mgrlnntg6o7kdvaklqu.jpg" alt="The thundering herd" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;jitter&lt;/strong&gt;: add a random amount to every wait, and the waves smear into a steady trickle. Stripe's own client libraries can do both for you, and attach an idempotency key to every request they retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole recipe
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqaey077m7ge2a59u204.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqaey077m7ge2a59u204.jpg" alt="The recipe" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Handle failures &lt;strong&gt;consistently&lt;/strong&gt;: clients retry.&lt;/li&gt;
&lt;li&gt;Handle failures &lt;strong&gt;safely&lt;/strong&gt;: servers are idempotent, using keys.&lt;/li&gt;
&lt;li&gt;Handle failures &lt;strong&gt;responsibly&lt;/strong&gt;: back off exponentially and add jitter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At Stripe, every &lt;code&gt;POST&lt;/code&gt; accepts an idempotency key. &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; don't need one: they're idempotent by definition.&lt;/p&gt;

&lt;p&gt;Watch the full animated breakdown: &lt;a href="https://youtu.be/OE_RnizyyEM" rel="noopener noreferrer"&gt;https://youtu.be/OE_RnizyyEM&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sources: Stripe's &lt;a href="https://stripe.com/blog/idempotency" rel="noopener noreferrer"&gt;Designing robust and predictable APIs with idempotency&lt;/a&gt;, the &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe API docs on idempotent requests&lt;/a&gt;, and Brandur Leach's &lt;a href="https://brandur.org/idempotency-keys" rel="noopener noreferrer"&gt;Implementing Stripe-like idempotency keys in Postgres&lt;/a&gt;. Maya, the charge ids and timings in the images are illustrative.&lt;/p&gt;

&lt;p&gt;Practice designing systems like this yourself at &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;systemdesignlab.in&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How Databases Survive Being Unplugged: Write-Ahead Logs Explained</title>
      <dc:creator>Vinayak Gaur</dc:creator>
      <pubDate>Fri, 02 Oct 2026 18:10:43 +0000</pubDate>
      <link>https://dev.to/system_design_lab/how-databases-survive-being-unplugged-write-ahead-logs-explained-27hh</link>
      <guid>https://dev.to/system_design_lab/how-databases-survive-being-unplugged-write-ahead-logs-explained-27hh</guid>
      <description>&lt;p&gt;By default, Postgres can leave your changed rows out of its table files for &lt;strong&gt;five minutes or more&lt;/strong&gt;. Commit a payment, pull the power cable one second later, and when the server comes back, the payment is still there.&lt;/p&gt;

&lt;p&gt;This post walks through how that works: the write-ahead log (WAL). It's the same idea in Postgres, SQLite, MySQL and almost every serious database. If you prefer video, here's the animated version:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/mhpTQN13bGI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The unplug test
&lt;/h2&gt;

&lt;p&gt;We'll use one example throughout. &lt;strong&gt;Asha sends Ravi ₹500.&lt;/strong&gt; To the database that's two changes: subtract 500 from Asha's balance, add 500 to Ravi's.&lt;/p&gt;

&lt;p&gt;Those changes happen in memory first, because memory is fast. But memory is volatile. Worse, imagine the power dies after the first change reaches disk but before the second one does: Asha has lost ₹500 and Ravi never got it.&lt;/p&gt;

&lt;p&gt;So a database has to promise two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All or nothing.&lt;/strong&gt; A transfer happens completely or not at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable.&lt;/strong&gt; Once it says "committed", the transfer survives a crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just write the tables?
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to write every changed page into the table files on every commit, and wait for the disk. It's slow and it still isn't safe.&lt;/p&gt;

&lt;p&gt;Postgres stores tables in &lt;strong&gt;8 KB pages&lt;/strong&gt;. Asha's row and Ravi's row live on different pages, and every index on those tables has its own pages too. One small transfer can touch four or five pages scattered across the disk. That's random I/O, on every commit. And if the power dies halfway through that list, some pages are new, some are old, and nothing records which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule: log first, data later
&lt;/h2&gt;

&lt;p&gt;Write-ahead logging replaces all of that with one rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before you change a data file, write down what you're about to change in a log, and make sure the log is on permanent storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For our transfer the log gets three short records: &lt;code&gt;asha -500&lt;/code&gt;, &lt;code&gt;ravi +500&lt;/code&gt;, and a &lt;strong&gt;commit record&lt;/strong&gt;. The moment the commit record is on disk, the transfer is durable. The table pages can stay in memory and be written whenever it's convenient.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7i2bc8mql37t2oyqb7dt.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7i2bc8mql37t2oyqb7dt.jpg" alt="The log gets three short records, and the commit makes it durable" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the log
&lt;/h2&gt;

&lt;p&gt;In Postgres the log is a series of files in &lt;code&gt;pg_wal&lt;/code&gt;, &lt;strong&gt;16 MB each&lt;/strong&gt; by default. Every record gets a &lt;strong&gt;log sequence number (LSN)&lt;/strong&gt;, which is just a byte position in the log, and it only goes up.&lt;/p&gt;

&lt;p&gt;Each data page remembers the LSN of the last change applied to it. That gives Postgres one more rule: a dirty page (changed in memory) can't be written to disk until the log is flushed at least up to that page's LSN. &lt;strong&gt;The log is always ahead of the data.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why writing twice is faster
&lt;/h2&gt;

&lt;p&gt;This is the part that surprises people. Writing everything twice (once to the log, later to the tables) beats forcing every table page to disk on every commit.&lt;/p&gt;

&lt;p&gt;The table writes were scattered. The log is written &lt;strong&gt;sequentially&lt;/strong&gt;: every new record goes on the end. So at commit time there's only one file to sync, the log. The scattered page writes still happen, but later, in the background, batched, where no user is waiting on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  fsync: the real promise
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;write()&lt;/code&gt; doesn't mean the data is on disk. The operating system usually just copies it into its page cache. To force it down to storage, the database calls &lt;strong&gt;&lt;code&gt;fsync&lt;/code&gt;&lt;/strong&gt;, which blocks until the OS says the data is on the device.&lt;/p&gt;

&lt;p&gt;So a commit really means: append the commit record, &lt;code&gt;fsync&lt;/code&gt; the log, and only then tell the client "done".&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6s8v07h5gs2ivt4bsv5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6s8v07h5gs2ivt4bsv5.jpg" alt="Five layers between your write and the disk" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are more caches below the OS: disk controllers and the drives themselves. The Postgres docs warn that many consumer drives and SSDs keep writes in a cache that doesn't survive a power failure, and some ignore flush commands. &lt;strong&gt;The log is only as honest as the disk under it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery: replay the log
&lt;/h2&gt;

&lt;p&gt;When the unplugged server boots, it runs crash recovery before accepting a single query. It reads a small control file, &lt;code&gt;pg_control&lt;/code&gt;, which points at the last checkpoint, then scans the log forward and &lt;strong&gt;redoes&lt;/strong&gt; every change it finds. If a page's LSN is already past a record, that change is skipped, so nothing is applied twice.&lt;/p&gt;

&lt;p&gt;Asha's transfer has a commit record on disk, so both halves are replayed and Ravi gets his ₹500.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetp0p7n5ya4eq8zzqdmb.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetp0p7n5ya4eq8zzqdmb.jpg" alt="Both halves are replayed" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the power had died before the commit record reached the log, the changes might even be replayed onto the pages, but Postgres treats the transaction as aborted, so nobody ever sees them. All or nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkpoints
&lt;/h2&gt;

&lt;p&gt;If the log grew forever, recovery would replay all of it and the disk would fill up. So Postgres runs &lt;strong&gt;checkpoints&lt;/strong&gt;: flush every dirty page to the table files, then write a checkpoint record. Everything before it is safely in the tables, recovery can start there, and old log files can be recycled.&lt;/p&gt;

&lt;p&gt;By default a checkpoint starts every &lt;strong&gt;5 minutes&lt;/strong&gt;, or sooner if the log is about to pass &lt;strong&gt;1 GB&lt;/strong&gt;. That's where the "five minutes" at the top comes from. Checkpoint more often and recovery is faster; less often and you do less write work in normal running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Torn pages
&lt;/h2&gt;

&lt;p&gt;There's one crash the log alone can't fix. An 8 KB page is &lt;strong&gt;sixteen 512-byte sector writes&lt;/strong&gt;. Lose power in the middle and you get a torn page: half new, half old, neither version. The log's small change records can't repair that.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo0e8cumybz20nklniv5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo0e8cumybz20nklniv5.jpg" alt="Half new, half old" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Postgres handles it with &lt;strong&gt;full page writes&lt;/strong&gt;: the first time a page is modified after a checkpoint, the whole page image goes into the log. Recovery restores that clean copy first, then replays the later changes on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group commit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fsync&lt;/code&gt; is the expensive step. When a thousand users commit at once, they don't need a thousand of them: their commit records sit next to each other at the end of the log, and &lt;strong&gt;one fsync makes all of them durable&lt;/strong&gt;. The busier the system, the more commits ride on each flush.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fvkd600lhpslx7n03qy.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fvkd600lhpslx7n03qy.jpg" alt="One fsync, many commits" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trading durability for speed
&lt;/h2&gt;

&lt;p&gt;Some workloads don't need every commit to wait for the disk. With &lt;code&gt;synchronous_commit = off&lt;/code&gt;, the client is told "done" before the log is flushed, and the WAL writer flushes it shortly after. It wakes every &lt;strong&gt;200 ms&lt;/strong&gt; by default, and the worst-case window of lost commits is three times that, about &lt;strong&gt;600 ms&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uan0w1e153ixp98asyh.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uan0w1e153ixp98asyh.jpg" alt="It can lose the tail, but not corrupt the database" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A crash can lose those last few commits, but it can't corrupt the database. Recovery still replays a clean, consistent log. You choose per transaction: Asha's payment keeps synchronous commit, a page-view counter can go async.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same log, everywhere
&lt;/h2&gt;

&lt;p&gt;SQLite has a WAL mode too: changes are appended to a &lt;code&gt;-wal&lt;/code&gt; file, a commit is a commit record, readers don't block the writer, and it checkpoints automatically at 1,000 pages (about 4 MB). And in Postgres, the same log powers point-in-time recovery and streaming replicas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A commit is durable when its &lt;strong&gt;log record&lt;/strong&gt; is on disk, not when the tables change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential&lt;/strong&gt; log writes are what make that cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoints&lt;/strong&gt; keep recovery short.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watch the full animated breakdown: &lt;a href="https://youtu.be/mhpTQN13bGI" rel="noopener noreferrer"&gt;https://youtu.be/mhpTQN13bGI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sources: the PostgreSQL docs on &lt;a href="https://www.postgresql.org/docs/current/wal-intro.html" rel="noopener noreferrer"&gt;WAL&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/wal-reliability.html" rel="noopener noreferrer"&gt;reliability&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/wal-configuration.html" rel="noopener noreferrer"&gt;WAL configuration&lt;/a&gt; and &lt;a href="https://www.postgresql.org/docs/current/wal-async-commit.html" rel="noopener noreferrer"&gt;async commit&lt;/a&gt;, and SQLite's &lt;a href="https://www.sqlite.org/wal.html" rel="noopener noreferrer"&gt;WAL page&lt;/a&gt;. Balances and LSNs in the images are illustrative.&lt;/p&gt;

&lt;p&gt;Practice designing systems like this yourself at &lt;a href="https://systemdesignlab.in" rel="noopener noreferrer"&gt;systemdesignlab.in&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>systemdesign</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
