<?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: speed engineer</title>
    <description>The latest articles on DEV Community by speed engineer (@speed_engineer).</description>
    <link>https://dev.to/speed_engineer</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%2F3844864%2F78a68c07-7a26-44f8-a98d-84d4d29fa7ef.png</url>
      <title>DEV Community: speed engineer</title>
      <link>https://dev.to/speed_engineer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/speed_engineer"/>
    <language>en</language>
    <item>
      <title>The Mental Model That Ended My Guess-and-Check Debugging</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Sun, 16 Aug 2026 03:41:57 +0000</pubDate>
      <link>https://dev.to/speed_engineer/the-mental-model-that-ended-my-guess-and-check-debugging-46lp</link>
      <guid>https://dev.to/speed_engineer/the-mental-model-that-ended-my-guess-and-check-debugging-46lp</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A few years ago I watched a senior engineer "debug" a slow checkout API for two hours. He added an index. No change. He bumped the connection pool. No change. He wrapped the handler in a cache. Marginal change, wrong reason. He was pattern-matching against past incidents instead of measuring the current one — and most of us do this more than we'd like to admit.&lt;/p&gt;

&lt;p&gt;The tell isn't lack of skill. It's the absence of a mental model that forces you to look at the machine before you touch the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Modern systems give you too many plausible culprits: database, network, GC pauses, lock contention, a noisy neighbor container, disk I/O. Without a framework, your brain defaults to "what fixed it last time," which is a bias, not a diagnosis. You end up changing five things and shipping a fix you can't actually explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;I now start every performance investigation with the USE method (credit to Brendan Gregg): for every resource — CPU, memory, disk, network — check three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Utilization&lt;/strong&gt;: is the resource busy?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturation&lt;/strong&gt;: is work queued waiting for it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors&lt;/strong&gt;: is it throwing errors that force retries or fallbacks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On that checkout API, here's what USE actually surfaced in about six minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vmstat 1
&lt;span class="go"&gt;procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 4  0      0 812340  20144 933212    0    0     0    18 1200 2400  8  3 60 29  0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wa&lt;/code&gt; (I/O wait) at 29% with CPU idle at 60% told me immediately this was not a CPU or code-logic problem — it was disk saturation. &lt;code&gt;iostat -x 1&lt;/code&gt; confirmed one EBS volume at 98% utilization with a queue depth climbing past 12. The "slow" endpoint was writing synchronous audit logs to the same volume as the primary database, and a batch job had started hammering that disk ten minutes earlier.&lt;/p&gt;

&lt;p&gt;No amount of query optimization or connection pool tuning was ever going to fix that, because the bottleneck wasn't in the code path at all — it was contention on a shared resource two layers down. The fix was moving the audit log writes to a separate volume. Twenty-minute change, once we knew where to look.&lt;/p&gt;

&lt;p&gt;The point isn't that USE always finds the answer that fast. The point is it stops you from guessing. You walk CPU, memory, disk, network in order, and for each one you either rule it out with data or you find your suspect. You never touch code before you've ruled out the machine.&lt;/p&gt;

&lt;p&gt;A second habit that pairs well with this: write down your hypothesis before you look at a single metric. "I think this is CPU-bound because the handler does JSON serialization in a loop." Then check. When the data disagrees with your hypothesis — and it will, more often than your ego wants — that gap is the actual lesson. I keep a running list of my wrong hypotheses next to my right ones. The wrong ones taught me more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Guess-and-check debugging is a pattern-matching bias, not a diagnostic process — it feels like progress without producing evidence.&lt;/li&gt;
&lt;li&gt;The USE method (Utilization, Saturation, Errors) forces you to check the machine's resources in order before touching application code.&lt;/li&gt;
&lt;li&gt;I/O wait time in &lt;code&gt;vmstat&lt;/code&gt; and queue depth in &lt;code&gt;iostat -x&lt;/code&gt; will tell you in minutes whether you have a disk-saturation problem — no code change will fix a hardware-contention issue.&lt;/li&gt;
&lt;li&gt;Write your hypothesis down before you look at data. The mismatches are where you actually learn.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>debugging</category>
      <category>systemdesign</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your Coding Interview Stopped Measuring Anything the Day Candidates Got Copilot</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Fri, 14 Aug 2026 04:04:07 +0000</pubDate>
      <link>https://dev.to/speed_engineer/your-coding-interview-stopped-measuring-anything-the-day-candidates-got-copilot-2fjk</link>
      <guid>https://dev.to/speed_engineer/your-coding-interview-stopped-measuring-anything-the-day-candidates-got-copilot-2fjk</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Last year I sat in on a loop for a senior backend role. The candidate solved a graph traversal problem in eleven minutes — clean code, right complexity, no hints needed. On paper, a hire. Ninety days later, on the job, the same person couldn't work out why a service was leaking connections under load. Not a knowledge gap — a reasoning gap. They'd never had to debug something they didn't already understand end to end.&lt;/p&gt;

&lt;p&gt;That disconnect isn't rare anymore. It's the default outcome of running a 2019-era interview loop in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;The classic algorithmic interview measures one thing well: can you recognize a known pattern (two-pointer, DP, BFS, sliding window) and implement it correctly under time pressure. That's a recall-and-execute task. It was never a great proxy for engineering judgment, but for a decade it correlated well enough with "smart, prepared, can code" to be useful.&lt;/p&gt;

&lt;p&gt;AI broke the correlation, not the test. Any candidate who's spent even a few months pairing with an AI assistant has effectively memorized the pattern library through repetition, whether or not they touch a model during the interview itself. The rehearsal loop got faster, so the "recall a pattern under pressure" signal compressed toward everyone scoring well. You're no longer measuring engineering ability — you're measuring how many hours someone spent grinding a fixed problem set, which is a much weaker signal and correlates poorly with what the job actually requires.&lt;/p&gt;

&lt;p&gt;Meanwhile the skill that was always the real differentiator — debugging code you didn't write, inside a system whose invariants you don't fully know, with incomplete information and someone waiting on you — never showed up in the interview at all. It's roughly 70-80% of senior engineering work by time spent, and it was zero percent of the assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;We rebuilt the loop around three changes, and the signal quality difference was immediate and obvious to every interviewer on the panel within the first week of running it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Replace "implement X" with "here's a broken 150-200 line service, find it."&lt;/strong&gt; We handed candidates real code (sanitized) with a bug that only reproduced under concurrency — a race between a cache invalidation and a read path. No algorithm to recall. Just: read unfamiliar code, form a hypothesis, test it, narrow it down. This is the actual daily loop of debugging in production, and it can't be shortcut by pattern memorization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Allow AI assistance explicitly, and interview the usage, not just the output.&lt;/strong&gt; We stopped pretending we could detect or prevent AI use and started treating it as a tool candidates would obviously have on the job. The question shifted from "did you use AI" to "when the AI suggested that fix, why did you accept it — and here's a case where its suggestion is subtly wrong, catch it." Candidates who understood the system could catch the wrong suggestion in seconds. Candidates who were pattern-matching couldn't, even with the AI's help, because they didn't have the mental model to evaluate what they were looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ask "why" mid-task, repeatedly, and watch the pause before the answer.&lt;/strong&gt; Not gotcha questions — just "why that line, why not the other approach." The hesitation pattern between someone reasoning live versus someone recalling a rehearsed justification is very distinguishable once you're listening for it. It's a soft signal, but paired with the debugging task it stopped being noisy.&lt;/p&gt;

&lt;p&gt;None of this eliminates false positives. It doesn't need to — it just needs to raise the correlation between loop performance and 90-day performance back above where the old format had fallen to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Algorithmic interviews measure pattern recall under time pressure — AI collapsed that signal by making rehearsal cheap and universal.&lt;/li&gt;
&lt;li&gt;The skill that predicts job performance — debugging unfamiliar code under partial information — was never directly tested by the old format, AI or not.&lt;/li&gt;
&lt;li&gt;Allowing AI explicitly and interviewing the judgment behind its use produces more signal than trying to detect or ban it.&lt;/li&gt;
&lt;li&gt;Watching how a candidate evaluates a wrong suggestion tells you more than watching them produce a correct one.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>interviewing</category>
      <category>ai</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Your Multithreaded Code Is Correct and Still 8x Slower: False Sharing Explained</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:01:35 +0000</pubDate>
      <link>https://dev.to/speed_engineer/your-multithreaded-code-is-correct-and-still-8x-slower-false-sharing-explained-1hb3</link>
      <guid>https://dev.to/speed_engineer/your-multithreaded-code-is-correct-and-still-8x-slower-false-sharing-explained-1hb3</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Eight cores. Eight threads. Each thread owns a private counter and increments it in a tight loop, millions of times a second, with zero locks and zero shared state at the logical level. No mutexes, no atomics contention, no obvious bottleneck in the code.&lt;/p&gt;

&lt;p&gt;Throughput should scale close to linearly. Instead you measure it and get roughly the same total throughput as one thread — sometimes worse. Nobody touched the same variable. There's no race condition. And it's still catastrophically slow.&lt;/p&gt;

&lt;p&gt;I hit this exact case profiling a stats-collection layer: a &lt;code&gt;Counter counters[NUM_THREADS]&lt;/code&gt; array, one slot per thread, each thread only ever writing its own index. On paper, embarrassingly parallel. In practice, adding cores made things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;The CPU doesn't move data in individual bytes or even individual variables — it moves it in cache lines, almost always 64 bytes on modern x86 and ARM. When a core writes to any byte in a cache line, the cache-coherency protocol (MESI, or a variant of it) invalidates every other core's cached copy of that &lt;em&gt;entire line&lt;/em&gt;, not just the byte that changed.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;int counters[8]&lt;/code&gt; array is 32 bytes. On a 64-byte cache line, all eight counters — one per thread — can live in a single cache line, or split across two. It doesn't matter that thread 3 only ever writes &lt;code&gt;counters[3]&lt;/code&gt; and never looks at &lt;code&gt;counters[5]&lt;/code&gt;. As far as the hardware is concerned, every write to that line by any core forces the other cores' copies to be invalidated and re-fetched.&lt;/p&gt;

&lt;p&gt;The result is the cache line physically ping-ponging between L1 caches across cores, even though there is zero logical data dependency between the threads. Each of those cross-core transfers costs on the order of tens to over a hundred nanoseconds — dwarfing the single-digit-nanosecond cost of the increment itself. You end up serializing on cache coherency traffic that never shows up as a lock, a mutex, or anything else you'd normally look for in a profiler's call graph.&lt;/p&gt;

&lt;p&gt;This is false sharing: a performance bug with no corresponding correctness bug. Your code is right. Your memory layout is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Detect it before you "fix" it.&lt;/strong&gt; Don't guess — false sharing has a specific fingerprint: HITM events (cache-line hit in "Modified" state, transferred cache-to-cache). On Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;perf c2c record &lt;span class="nt"&gt;--&lt;/span&gt; ./your_binary
perf c2c report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will point you at the exact cache line and the specific cores/threads fighting over it. Intel VTune's memory-access analysis surfaces the same signal on non-Linux setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pad or align the hot data to cache-line boundaries.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;alignas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PaddedCounter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;PaddedCounter&lt;/span&gt; &lt;span class="n"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NUM_THREADS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each counter owns its own cache line. No cross-core invalidation traffic, because no two threads' hot variables share a line anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prefer thread-local accumulation over shared arrays when you can.&lt;/strong&gt; Instead of N threads writing into a shared array of N slots, give each thread a genuinely private (thread-local or stack-local) counter and aggregate once at the end. This sidesteps the layout problem entirely instead of papering over it with padding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don't pad everything reflexively.&lt;/strong&gt; Padding trades memory and cache footprint for coherency traffic. On data that's read-mostly, or accessed by a single thread anyway, padding just wastes cache capacity and can hurt performance elsewhere. Measure with &lt;code&gt;perf c2c&lt;/code&gt; first, pad the specific structures it flags, and stop there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;False sharing produces zero logic errors — your tests pass, your assertions hold, and your scaling is still terrible.&lt;/li&gt;
&lt;li&gt;The unit of cache coherency is the cache line (typically 64 bytes), not the variable — layout decisions you never think about become hardware-level contention.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;perf c2c&lt;/code&gt; turns "this is inexplicably slow" into "these two cores are fighting over this exact cache line," which is the difference between guessing and fixing.&lt;/li&gt;
&lt;li&gt;Padding and thread-local accumulation are targeted fixes for a measured problem, not a default you apply to every struct.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>concurrency</category>
      <category>computerscience</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Hyrum's Law: Why You Can Never Actually Deprecate Anything</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Sun, 09 Aug 2026 03:39:47 +0000</pubDate>
      <link>https://dev.to/speed_engineer/hyrums-law-why-you-can-never-actually-deprecate-anything-1c6k</link>
      <guid>https://dev.to/speed_engineer/hyrums-law-why-you-can-never-actually-deprecate-anything-1c6k</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;We shipped a "deprecated" internal field, &lt;code&gt;response.legacy_id&lt;/code&gt;, and left it in for backward compatibility — marked deprecated in the docs, with a removal date two releases out. Nobody consumed it directly; no client code referenced it anywhere. So we cut it.&lt;/p&gt;

&lt;p&gt;Three unrelated services broke in production within about forty minutes. Not because they read the field. Because a shared caching proxy fingerprinted the &lt;em&gt;entire response body&lt;/em&gt; to build cache keys, and removing a field changed every hash, which invalidated caches that three completely different systems depended on for reasons that had nothing to do with &lt;code&gt;legacy_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nobody used the field. Everybody depended on it anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;This has a name — Hyrum's Law, coined by Hyrum Wright at Google: "With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody."&lt;/p&gt;

&lt;p&gt;The key word is &lt;em&gt;observable&lt;/em&gt;, not &lt;em&gt;documented&lt;/em&gt;. Your contract says "this field is deprecated, don't rely on it." Reality doesn't care what the contract says. If a behavior is observable — response ordering, field presence, error message text, timing, byte-for-byte JSON shape, even header casing — someone, somewhere, at scale, will build on it. Usually not maliciously. Usually accidentally, through a generic tool (a diffing layer, a schema validator, a caching proxy, a test snapshot) that treats the entire observable surface as the contract, because that's the only contract it can see.&lt;/p&gt;

&lt;p&gt;The bigger your user base, the more certain this becomes. At 10 callers, a dependency on field ordering is a coincidence. At 10,000, it's a statistical certainty that it exists somewhere.&lt;/p&gt;

&lt;p&gt;This is also why "nobody's using it, I checked the code" is a weaker guarantee than it feels. You checked the code that reads your API directly. You didn't check every downstream cache key, diff tool, monitoring rule, or test fixture built against your response shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;You can't design your way out of Hyrum's Law — you can only manage the blast radius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version your contract explicitly, and make it narrower than what you actually return.&lt;/strong&gt; If your spec only promises three fields, undocumented extra fields are fair game to change — but say so loudly, because "undocumented" doesn't mean "unobserved."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make deprecation observable before it's real.&lt;/strong&gt; Don't silently remove a field. First return it with a sentinel value for a full release cycle, log every caller that still reads a non-null value, and ship machine-readable deprecation signals (like &lt;code&gt;Sunset&lt;/code&gt;/&lt;code&gt;Deprecation&lt;/code&gt; headers) instead of a line in a changelog nobody reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change shape deliberately, not incidentally.&lt;/strong&gt; If you're removing a field, make it its own atomic change — not bundled with an unrelated refactor — so if something breaks, the diff that caused it is one commit, not forty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat internal consumers with the same discipline as external ones.&lt;/strong&gt; The break above wasn't a public API — it was internal service-to-service traffic, which teams often skip versioning discipline on because "we control both sides." You control both &lt;em&gt;codebases&lt;/em&gt;. You don't control every cache and proxy sitting between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assume the null hypothesis is wrong.&lt;/strong&gt; Before removing any observable behavior, default to "something depends on this," not "probably nothing depends on this." That's cheap to disprove with real production traffic sampling, and expensive to discover the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hyrum's Law: at scale, every observable behavior of your system becomes a dependency, whether you documented it or not.&lt;/li&gt;
&lt;li&gt;"Nobody calls this field" only accounts for direct callers — indirect consumers (caches, diff tools, snapshots, monitors) depend on the whole observable shape.&lt;/li&gt;
&lt;li&gt;Deprecation should be logged and observable before it's enforced; silent removal is where the surprises live.&lt;/li&gt;
&lt;li&gt;Internal APIs need the same versioning discipline as public ones — "we control both sides" isn't the same as "we control every intermediary."&lt;/li&gt;
&lt;li&gt;Default to assuming something depends on the behavior you're about to change, and verify against real traffic before you cut it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>api</category>
      <category>engineering</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The UDP Buffer Nobody Tunes: How a 256KB Kernel Default Cost Us 30% of Our Packets</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Sat, 08 Aug 2026 03:46:51 +0000</pubDate>
      <link>https://dev.to/speed_engineer/the-udp-buffer-nobody-tunes-how-a-256kb-kernel-default-cost-us-30-of-our-packets-map</link>
      <guid>https://dev.to/speed_engineer/the-udp-buffer-nobody-tunes-how-a-256kb-kernel-default-cost-us-30-of-our-packets-map</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A video platform I worked on wasn't crashing — it was worse than that. Calls stayed "up" but froze every few seconds, audio turned robotic, and it only happened between 5 and 7 PM. WebRTC sessions were logging close to 30% packet loss during that window. Network ops checked switches, routers, bandwidth — utilization sat around 40%, nothing congested. Ticket closed: "not a network problem."&lt;/p&gt;

&lt;p&gt;It was. Just not in the place anyone was looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;The instinct with UDP is to treat it like "TCP without the reliability guarantees." That framing hides the part that actually matters: TCP has backpressure built in. When a TCP receive buffer fills, the receiver shrinks its window and the sender slows down. UDP has none of that. The kernel gets a datagram, tries to place it in a receive buffer, and if there's no room, it silently drops it — no retry, no signal to the sender, nothing. From the app's point of view, the packet simply never existed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tcpdump&lt;/code&gt; showed packets arriving cleanly at the NIC. Application logs showed holes in the sequence numbers. Packets were vanishing somewhere between the wire and the process — which meant the kernel, not the code, was the suspect.&lt;/p&gt;

&lt;p&gt;The confirming metric was &lt;code&gt;udp_receive_buffer_errors&lt;/code&gt;, sitting at zero off-peak and spiking into the thousands-per-second exactly when complaints rolled in. &lt;code&gt;netstat -su&lt;/code&gt; made it explicit: 223,401 receive buffer errors out of ~752K packets received. The Ubuntu default for &lt;code&gt;net.core.rmem_default&lt;/code&gt; was 212,992 bytes — roughly 256 KB. At peak, the service was pushing ~50,000 packets/sec at ~1,200 bytes each: about 60 MB/sec into a 256 KB bucket. That's roughly 4 milliseconds of headroom before the buffer fills. Get descheduled for longer than that — completely normal under load — and the kernel starts discarding.&lt;/p&gt;

&lt;p&gt;It compounds, too: WebRTC detects the quality drop from the loss and adds redundancy and retries at the application layer, which throws more traffic at the same undersized buffer. The fix for congestion becomes the thing that deepens it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;The fix is two layers, and both are required — most teams only do one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kernel layer&lt;/strong&gt; — raise the ceiling and the default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/sysctl.conf&lt;/span&gt;
net.core.rmem_max &lt;span class="o"&gt;=&lt;/span&gt; 16777216      &lt;span class="c"&gt;# 16 MB max&lt;/span&gt;
net.core.rmem_default &lt;span class="o"&gt;=&lt;/span&gt; 16777216  &lt;span class="c"&gt;# 16 MB default for new sockets&lt;/span&gt;

sysctl &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;16 MB instead of 256 KB turns 4ms of headroom into roughly 260ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application layer&lt;/strong&gt; — the socket still has to ask for it, and you have to verify what it actually got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SOCK_DGRAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;buffer_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;setsockopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SOL_SOCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SO_RCVBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buffer_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer_size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to set socket receive buffer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;actual_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;socklen_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;getsockopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SOL_SOCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SO_RCVBUF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;actual_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Requested: %d bytes, Got: %d bytes&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;getsockopt&lt;/code&gt; check matters more than it looks — some distros silently double what you request for internal bookkeeping, so "did I get what I asked for" is not a safe assumption.&lt;/p&gt;

&lt;p&gt;One dead end worth naming: bumping the packet-processing thread's scheduling priority. It seems logical — drain the buffer faster — but it starves other threads of CPU time, and now you've traded packet loss for database timeouts. Buffer sizing is the correct first move; thread priority is a last resort, not a first instinct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UDP has zero backpressure — a full receive buffer means silent, unrecoverable loss, not slowdown.&lt;/li&gt;
&lt;li&gt;Kernel socket buffer defaults (&lt;code&gt;net.core.rmem_default&lt;/code&gt;) are generic and almost never sized for sustained high-throughput UDP.&lt;/li&gt;
&lt;li&gt;Fixing it requires both the sysctl change and an explicit &lt;code&gt;setsockopt(SO_RCVBUF)&lt;/code&gt; call in your app — plus a &lt;code&gt;getsockopt&lt;/code&gt; check to confirm what you actually got.&lt;/li&gt;
&lt;li&gt;Watch &lt;code&gt;udp_receive_buffer_errors&lt;/code&gt; from &lt;code&gt;netstat -su&lt;/code&gt; as a standing metric; it should be zero, and any sustained non-zero value is a real signal, not noise.&lt;/li&gt;
&lt;li&gt;Think in bursts, not averages — a tame req/sec number can still overflow a small buffer during a 200ms spike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote up the full debugging story — including the wrong turn I took first and what I now monitor because of it — &lt;a href="https://medium.com/@speed_enginner/networking-for-developers-i-lost-30-of-udp-packets-the-debugging-story-f755f5680b35" rel="noopener noreferrer"&gt;on Medium&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>linux</category>
      <category>performance</category>
      <category>devops</category>
    </item>
    <item>
      <title>Lost in the Middle: Why Feeding Your Agent More Context Makes It Dumber</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Wed, 05 Aug 2026 05:06:00 +0000</pubDate>
      <link>https://dev.to/speed_engineer/lost-in-the-middle-why-feeding-your-agent-more-context-makes-it-dumber-1ac</link>
      <guid>https://dev.to/speed_engineer/lost-in-the-middle-why-feeding-your-agent-more-context-makes-it-dumber-1ac</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;You build a RAG pipeline. You test it with 3 retrieved documents and the correct answer is right there in document 2 - the model nails it every time. Feeling good, you bump &lt;code&gt;top_k&lt;/code&gt; from 3 to 15 "to be safe," figuring more context can only help.&lt;/p&gt;

&lt;p&gt;Accuracy drops. Not a crash, not a timeout, no error in your logs - the model just starts confidently giving wrong answers, or missing facts that are sitting in plain text inside the prompt you sent it. You re-read the context window by hand and the answer is &lt;em&gt;right there&lt;/em&gt;. The model saw it. It just didn't use it.&lt;/p&gt;

&lt;p&gt;If you've hit this, you didn't build a bad retriever. You ran into a well-documented, model-agnostic failure mode: LLMs don't recall information uniformly across a context window. They recall the beginning and the end. The middle is where information goes to die.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;This isn't a rumor - it's been measured directly. Liu et al.'s "Lost in the Middle" study (2023) ran controlled retrieval tests across context lengths and found a consistent U-shaped accuracy curve: performance is highest when the relevant fact sits at the very start or very end of the context, and drops - sometimes below random-guessing-adjacent territory - when it's buried in the middle. This held across multiple model families, not one vendor's quirk.&lt;/p&gt;

&lt;p&gt;The mechanism is architectural, not a bug you can patch. Self-attention doesn't distribute recall evenly across token positions. Models get heavy exposure to short-range dependencies during training (the next word usually depends on nearby words), and comparatively little training signal that rewards precise retrieval from the geometric middle of a long, unstructured span. Positional encodings compound this - most schemes bias attention toward strong local recency and, separately, toward the sequence start (a natural anchor point), leaving the middle structurally under-attended.&lt;/p&gt;

&lt;p&gt;Here's the part that actually bites in production: the advertised context window and the &lt;em&gt;effectively usable&lt;/em&gt; context window are different numbers, and vendors report the former. A model with a "1M token context" doesn't mean it retrieves reliably across 1M tokens - it means it doesn't error out before 1M tokens. Those are not the same claim, and the gap between them is exactly where "it worked in the demo, it's flaky in prod" bugs come from.&lt;/p&gt;

&lt;p&gt;And it's silent by design. There's no exception to catch. Your retriever did its job, your prompt assembly did its job, the tokens are unambiguously present in the context - the failure is purely in what the model chooses to attend to, which is invisible from the outside unless you're specifically testing for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stop treating &lt;code&gt;top_k&lt;/code&gt; as a safety dial.&lt;/strong&gt; More retrieved chunks doesn't monotonically improve recall - past a point it actively pushes your one correct chunk further into the dead zone. Tune &lt;code&gt;top_k&lt;/code&gt; down, not up, and measure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reorder, don't just retrieve.&lt;/strong&gt; After ranking, place your highest-confidence chunks at the start and end of the context, not in ranked order top-to-bottom. A chunk ranked #1 by your retriever but positioned dead-center in the prompt will underperform a chunk ranked #4 placed at the edges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a reranker stage.&lt;/strong&gt; A cheap cross-encoder rerank pass after initial retrieval, feeding only the top few into the prompt in a position-aware order, consistently outperforms "retrieve broadly, stuff it all in, let the model sort it out."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build your own needle-in-a-haystack eval.&lt;/strong&gt; Don't trust a vendor's long-context benchmark for your workload. Position sensitivity varies by model, by model &lt;em&gt;version&lt;/em&gt; (it can regress on a silent upgrade), and by the structure of your specific documents. Take a real query from your logs, plant the known-correct fact at position 10%, 50%, and 90% of your typical context length, and measure accuracy at each. Rerun it every time you swap models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider narrowing before you widen.&lt;/strong&gt; A multi-step retrieval loop that progressively filters down to a small, high-confidence context often beats a single giant context stuffed with "everything that might be relevant." Fewer, better-placed tokens beat more tokens almost every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Context window size and effective retrieval range are different numbers - don't conflate them.&lt;/li&gt;
&lt;li&gt;The "Lost in the Middle" effect is architectural and measured across model families, not a one-off bug.&lt;/li&gt;
&lt;li&gt;The failure mode is silent: no error, just quietly wrong answers on facts that are technically present.&lt;/li&gt;
&lt;li&gt;Fixes: shrink &lt;code&gt;top_k&lt;/code&gt;, rerank and position-aware reorder, build your own positional eval, and prefer narrowing retrieval over widening it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why Your p99 Latency Explodes Long Before Your Servers Look Busy</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Mon, 03 Aug 2026 04:47:12 +0000</pubDate>
      <link>https://dev.to/speed_engineer/why-your-p99-latency-explodes-long-before-your-servers-look-busy-3fo6</link>
      <guid>https://dev.to/speed_engineer/why-your-p99-latency-explodes-long-before-your-servers-look-busy-3fo6</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A service humming along at 40ms average latency. CPU sits at 60%. Dashboards green. Then the pager goes off: p99 is 900ms, and it's been that way for weeks — nobody noticed because the average never moved. On-call pulls up flame graphs, finds nothing pathological, and closes the ticket as "noise."&lt;/p&gt;

&lt;p&gt;It isn't noise. It's queueing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Average latency measures the common case. Tail latency measures what happens when several unlikely things line up at once — and unlikely things line up more often than intuition suggests. Two mechanisms do almost all the damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Utilization is not linear with latency.&lt;/strong&gt; For a simple queue (the M/M/1 approximation), expected wait time scales roughly with ρ/(1-ρ), where ρ is utilization. At 50% utilization, that multiplier is 1x. At 80%, it's 4x. At 95%, it's 19x. At 99%, it's 99x. The curve stays flat for a long time and then goes vertical. A service that "looks fine" at 60% CPU isn't 60% away from saturation — it's most of the way up a curve that's about to explode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fan-out amplifies tails.&lt;/strong&gt; If a request fans out to 20 backend calls and waits on all of them, your response time is the &lt;em&gt;max&lt;/em&gt; of 20 samples, not the average of 20. Even if every backend has a p99 of 100ms (99% chance of finishing under 100ms), the odds that &lt;em&gt;all 20&lt;/em&gt; finish under 100ms is 0.99^20 ≈ 82%. Close to 1 in 5 requests gets dragged down by whichever one of the twenty had a bad moment. That's not a bug — it's arithmetic. Add naive retries on top and you can amplify load at exactly the moment a dependency is already struggling.&lt;/p&gt;

&lt;p&gt;The two mechanisms compound: fan-out multiplies your exposure to tail events, and rising utilization on any one dependency makes tail events on that dependency more frequent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stop planning capacity around average utilization.&lt;/strong&gt; Plan around the utilization at which queueing delay becomes unacceptable, then keep steady-state usage well under that line — most latency-sensitive paths want something like 65-75%, not 90%+.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound fan-out instead of letting it grow.&lt;/strong&gt; Every extra parallel call you wait on is another chance to hit someone else's bad millisecond. If you don't need all 20 results, don't wait for all 20.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use hedged (backup) requests instead of blind retries.&lt;/strong&gt; Fire a second request to a different replica if the first hasn't returned by roughly your own p95, and take whichever comes back first, cancelling the loser. This trims the tail without the pile-on effect of retry-on-timeout, which just adds load to an already-struggling dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add jitter to timeouts and retries.&lt;/strong&gt; Synchronized retries across many clients are how a small blip becomes a cascading outage — everyone times out at the same millisecond and hits the backend at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolate noisy neighbors.&lt;/strong&gt; A single slow dependency behind a shared thread or connection pool can drag down requests that never even touch it, because everything is waiting in the same queue for a free worker. Bulkhead pools per-dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Average and tail latency are different problems with different causes. A flat average tells you nothing about how close you are to the queueing cliff.&lt;/li&gt;
&lt;li&gt;Utilization near saturation makes latency blow up nonlinearly — plan capacity against that curve, not against "CPU still has headroom."&lt;/li&gt;
&lt;li&gt;Fan-out to N parallel calls means your latency is governed by the worst of N, not the average of N. Bound N wherever you can.&lt;/li&gt;
&lt;li&gt;Hedged requests with jittered timeouts beat blind retries for taming tails without triggering retry storms.&lt;/li&gt;
&lt;li&gt;If your p99 is bad and your average looks fine, check utilization and fan-out width before you check code.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>systemdesign</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Your p99 Latency Explodes Long Before Your Servers Look Busy</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Mon, 03 Aug 2026 04:46:39 +0000</pubDate>
      <link>https://dev.to/speed_engineer/why-your-p99-latency-explodes-long-before-your-servers-look-busy-1aob</link>
      <guid>https://dev.to/speed_engineer/why-your-p99-latency-explodes-long-before-your-servers-look-busy-1aob</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A service humming along at 40ms average latency. CPU sits at 60%. Dashboards green. Then the pager goes off: p99 is 900ms, and it's been that way for weeks — nobody noticed because the average never moved. On-call pulls up flame graphs, finds nothing pathological, and closes the ticket as "noise."&lt;/p&gt;

&lt;p&gt;It isn't noise. It's queueing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Average latency measures the common case. Tail latency measures what happens when several unlikely things line up at once — and unlikely things line up more often than intuition suggests. Two mechanisms do almost all the damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Utilization is not linear with latency.&lt;/strong&gt; For a simple queue (the M/M/1 approximation), expected wait time scales roughly with ρ/(1-ρ), where ρ is utilization. At 50% utilization, that multiplier is 1x. At 80%, it's 4x. At 95%, it's 19x. At 99%, it's 99x. The curve stays flat for a long time and then goes vertical. A service that "looks fine" at 60% CPU isn't 60% away from saturation — it's most of the way up a curve that's about to explode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fan-out amplifies tails.&lt;/strong&gt; If a request fans out to 20 backend calls and waits on all of them, your response time is the &lt;em&gt;max&lt;/em&gt; of 20 samples, not the average of 20. Even if every backend has a p99 of 100ms (99% chance of finishing under 100ms), the odds that &lt;em&gt;all 20&lt;/em&gt; finish under 100ms is 0.99^20 ≈ 82%. Close to 1 in 5 requests gets dragged down by whichever one of the twenty had a bad moment. That's not a bug — it's arithmetic. Add naive retries on top and you can amplify load at exactly the moment a dependency is already struggling.&lt;/p&gt;

&lt;p&gt;The two mechanisms compound: fan-out multiplies your exposure to tail events, and rising utilization on any one dependency makes tail events on that dependency more frequent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stop planning capacity around average utilization.&lt;/strong&gt; Plan around the utilization at which queueing delay becomes unacceptable, then keep steady-state usage well under that line — most latency-sensitive paths want something like 65-75%, not 90%+.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound fan-out instead of letting it grow.&lt;/strong&gt; Every extra parallel call you wait on is another chance to hit someone else's bad millisecond. If you don't need all 20 results, don't wait for all 20.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use hedged (backup) requests instead of blind retries.&lt;/strong&gt; Fire a second request to a different replica if the first hasn't returned by roughly your own p95, and take whichever comes back first, cancelling the loser. This trims the tail without the pile-on effect of retry-on-timeout, which just adds load to an already-struggling dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add jitter to timeouts and retries.&lt;/strong&gt; Synchronized retries across many clients are how a small blip becomes a cascading outage — everyone times out at the same millisecond and hits the backend at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolate noisy neighbors.&lt;/strong&gt; A single slow dependency behind a shared thread or connection pool can drag down requests that never even touch it, because everything is waiting in the same queue for a free worker. Bulkhead pools per-dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Average and tail latency are different problems with different causes. A flat average tells you nothing about how close you are to the queueing cliff.&lt;/li&gt;
&lt;li&gt;Utilization near saturation makes latency blow up nonlinearly — plan capacity against that curve, not against "CPU still has headroom."&lt;/li&gt;
&lt;li&gt;Fan-out to N parallel calls means your latency is governed by the worst of N, not the average of N. Bound N wherever you can.&lt;/li&gt;
&lt;li&gt;Hedged requests with jittered timeouts beat blind retries for taming tails without triggering retry storms.&lt;/li&gt;
&lt;li&gt;If your p99 is bad and your average looks fine, check utilization and fan-out width before you check code.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>systemdesign</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Backups Have Been Silently Corrupt Longer Than Your Retention Window</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Sat, 01 Aug 2026 06:56:26 +0000</pubDate>
      <link>https://dev.to/speed_engineer/your-backups-have-been-silently-corrupt-longer-than-your-retention-window-2b17</link>
      <guid>https://dev.to/speed_engineer/your-backups-have-been-silently-corrupt-longer-than-your-retention-window-2b17</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Corruption gets discovered on read, almost always. A report pulls a file, the numbers look wrong, someone finally opens the raw bytes and finds the sector is garbage. Fine — that's what checksums are for. You confirm the file is bad, you go pull it from backup, and you breathe out.&lt;/p&gt;

&lt;p&gt;Then the restored copy has the exact same corruption.&lt;/p&gt;

&lt;p&gt;So does the one from three days before that. So does the one from three weeks before that. You don't have a "restore from backup" problem anymore. You have a "how far back do I have to go to find a good copy, and do I still have one" problem — and the honest answer, more often than teams expect, is no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Almost every system checksums on &lt;strong&gt;read&lt;/strong&gt;: verify the bytes when something asks for them, so you at least know not to trust a bad file. Far fewer systems checksum on &lt;strong&gt;write&lt;/strong&gt;: verify the bytes the moment they're stored, so corruption gets caught within seconds of happening instead of whenever someone next happens to need that exact object.&lt;/p&gt;

&lt;p&gt;That gap between "corruption occurs" and "corruption is detected" is the dwell time. Nothing about a backup process shortens it. A backup job doesn't know what "correct" looks like — it just faithfully, efficiently copies whatever bytes are sitting there, intact or not. If the source was already corrupted when last night's backup ran, you now have a corrupted backup, indistinguishable from a good one, sitting in your retention window right next to a dozen other backups with the identical problem.&lt;/p&gt;

&lt;p&gt;The failure only becomes visible once dwell time exceeds retention window. A firmware bug flips bits in a file on day one. Nobody reads that specific file again until day forty-five, for a quarterly report. If your retention is thirty days, every single backup you're holding — all thirty of them — postdates the corruption. There is no "go back further" option, because further back doesn't exist anymore. The backup succeeded every night. The system was never lying to you about that. It just was never checking the thing you actually needed it to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Checksum at write time, not just read time.&lt;/strong&gt; Compute and store a checksum (or use a filesystem/object store that does this natively — ZFS, Btrfs, S3's built-in integrity checks) the moment data lands. Verify on every read after that, and alert immediately on a mismatch — don't wait for someone to notice the number looks off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run scheduled integrity scans, not just on-demand checks.&lt;/strong&gt; A read-triggered checksum only catches corruption in data someone actually reads. Cold data — old logs, archives, rarely-touched records — can sit corrupted indefinitely with nothing ever tripping the check. A periodic full-corpus scrub (ZFS &lt;code&gt;scrub&lt;/code&gt;, or a scheduled job that reads and re-verifies every object) forces detection on a schedule you control, instead of waiting on read patterns you don't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Size your scrub interval against your retention window, deliberately, as a stated relationship — not two settings that happen to coexist.&lt;/strong&gt; If backups are kept 30 days, your worst-case detection time — the longest possible gap before a scrub or a read would catch a given piece of corruption — needs to be meaningfully shorter than 30 days. If it isn't, you don't actually have 30 days of real recovery coverage; you have some smaller, undefined number that depends on when corruption happens to occur relative to your scan cycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep at least one backup generation older than your detection SLA, treated as untouchable.&lt;/strong&gt; Even with fast detection, you want a fallback that predates your worst plausible dwell time — a monthly or quarterly snapshot held outside the normal rotation, specifically so "go back further" is still a real option the day you need it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Checksums that only run on read tell you a file is bad; they don't limit how long it's been bad, and that gap is what actually determines whether backups can save you.&lt;/li&gt;
&lt;li&gt;A backup process has no concept of "correct" — it copies whatever bytes exist, so corruption propagates through every generation made after it occurred.&lt;/li&gt;
&lt;li&gt;The real risk metric isn't "do we have backups," it's "is our worst-case corruption dwell time shorter than our retention window" — most teams have never measured this.&lt;/li&gt;
&lt;li&gt;Write-time checksums plus scheduled full-corpus scrubbing are what actually bound dwell time; read-triggered checks alone leave cold data unprotected indefinitely.&lt;/li&gt;
&lt;li&gt;Keep one backup generation deliberately older than your detection SLA as an untouchable fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote about the mechanics of catching corruption in the first place — checksum algorithms, where to place the check, what it costs — on Medium: &lt;a href="https://medium.com/@speed_enginner/checksum-everything-corruption-caught-before-catastrophe-5cace12122fa" rel="noopener noreferrer"&gt;https://medium.com/@speed_enginner/checksum-everything-corruption-caught-before-catastrophe-5cace12122fa&lt;/a&gt;&lt;/p&gt;

</description>
      <category>storage</category>
      <category>backend</category>
      <category>architecture</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Your Hiring Freeze Has an Adverse Selection Problem</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Fri, 31 Jul 2026 05:24:55 +0000</pubDate>
      <link>https://dev.to/speed_engineer/your-hiring-freeze-has-an-adverse-selection-problem-3iji</link>
      <guid>https://dev.to/speed_engineer/your-hiring-freeze-has-an-adverse-selection-problem-3iji</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every hiring freeze I've watched up close follows the same script: leadership announces a freeze, comp reviews get "paused pending Q results," and six months later someone asks why the team feels weaker than it did a year ago — even though the attrition numbers look fine on a dashboard.&lt;/p&gt;

&lt;p&gt;The dashboard is lying. Not with fake numbers, but with the wrong number. Total attrition can sit at a perfectly normal 8-10% while the composition of who's leaving quietly inverts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;A freeze almost never freezes just headcount. It freezes comp too — off-cycle adjustments get shelved "until things stabilize." Meanwhile the external market for the same role doesn't freeze at all. Two years of that gap and you have real comp compression: a senior engineer hired externally today would clear a number your three-year tenured senior engineer is nowhere near.&lt;/p&gt;

&lt;p&gt;Here's the part that doesn't show up in an attrition chart: the tenured engineer's ability to &lt;em&gt;act&lt;/em&gt; on that gap is not evenly distributed. Your strongest performers pass external loops easily — they have a portfolio, a track record, recruiters already in their inbox. A credible outside offer is a phone call away. Your average-to-below-average performers don't have that option nearly as reliably; external interviews are a real filter, and they know it.&lt;/p&gt;

&lt;p&gt;So when comp quietly falls behind market during a freeze, the population that's &lt;em&gt;able&lt;/em&gt; to leave and the population that's &lt;em&gt;underpaid&lt;/em&gt; only partially overlap — and the overlap skews toward your best people. This is adverse selection, the same mechanic Akerlof described in the market for lemons, just running on your internal talent pool instead of used cars: the participants with better private information about their own value are the ones who exit the pool, leaving a residual population that's systematically weaker than the average you started with.&lt;/p&gt;

&lt;p&gt;It compounds. The freeze didn't shrink the workload, so the remaining team — now missing its strongest members — absorbs it. More load lands on a team with a lower average ceiling, which raises the burnout and attrition risk of exactly the people you have left. That's not a one-time dip in team quality. It's a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Three things I've seen actually work, none of which require lifting the freeze:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track regretted attrition separately from total attrition.&lt;/strong&gt; A flat 9% churn number can hide a regretted-attrition rate that's climbing fast. If you're only watching the aggregate, you find out about the problem a year after it started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compute compa-ratio against current external market data, not last year's band.&lt;/strong&gt; Bands set eighteen months ago are exactly the thing comp compression exploits. If you're benchmarking against a stale band, you'll conclude you're "fine" right up until your best engineer gives notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spend retention budget as a scalpel, not a freeze.&lt;/strong&gt; A blanket comp freeze and a handful of targeted off-cycle adjustments for flight-risk high performers cost very differently, and only one of them stops the adverse-selection loop. The org that can't do targeted retention during a freeze is choosing, whether it says so or not, to let the loop run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A stable total-attrition number can coexist with a rapidly worsening &lt;em&gt;composition&lt;/em&gt; of who's leaving.&lt;/li&gt;
&lt;li&gt;Comp compression during a freeze isn't just "everyone underpaid equally" — the people most able to act on it are disproportionately your strongest performers.&lt;/li&gt;
&lt;li&gt;This is adverse selection: the exit is correlated with ability, not random.&lt;/li&gt;
&lt;li&gt;Fix the measurement first (regretted attrition, live compa-ratio) — you can't manage a loop you can't see.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>leadership</category>
      <category>management</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The CPU Graph Said 40%. We Were Being Throttled Every Second.</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:01:52 +0000</pubDate>
      <link>https://dev.to/speed_engineer/the-cpu-graph-said-40-we-were-being-throttled-every-second-33oe</link>
      <guid>https://dev.to/speed_engineer/the-cpu-graph-said-40-we-were-being-throttled-every-second-33oe</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;3 AM, paged for p99 latency on a checkout service that was, by every dashboard we had, fine. Average CPU utilization: 38%. Memory: comfortable. Error rate: near zero. And yet roughly one request in twenty was taking 800ms-1200ms instead of the usual 40ms, in bursts that lasted a few seconds and then vanished.&lt;/p&gt;

&lt;p&gt;We did what everyone does first. Checked GC logs — no long pauses. Checked the database — query times were flat. Checked the network — no retries, no timeouts upstream. Checked lock contention in the app — nothing held for more than a few microseconds. Every obvious suspect had an alibi.&lt;/p&gt;

&lt;p&gt;The graph that was supposedly clearing us — CPU usage — was the thing lying to us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Kubernetes (and Docker, and anything built on cgroups) doesn't enforce a CPU limit as "don't average more than X cores." It enforces it as a hard budget inside the CFS (Completely Fair Scheduler) bandwidth controller: every 100ms accounting period, your container gets a fixed slice of CPU-time — say 200ms of combined core-time for a "2 CPU" limit — and once that slice is spent, every thread in the container is frozen until the next period starts. No exceptions, no borrowing from a quiet neighbor period.&lt;/p&gt;

&lt;p&gt;That period is 100ms. Your dashboard is almost certainly scraping and averaging over 15s, 30s, or 60s windows. A service can burn its entire 100ms quota in the first 8-10ms of a period — because a burst of concurrent requests briefly spun up enough threads to blow through the budget — sit frozen for the remaining ~90ms, and repeat that dozens of times a second. Averaged over a minute, that's "38% CPU usage." Inside any individual 100ms window, it's 100% then 0%, over and over, and every request that happened to land in a frozen window paid for it in latency.&lt;/p&gt;

&lt;p&gt;This is the part that trips up even experienced engineers: CPU limits cap peak usage inside a tiny accounting window, not usage averaged over the window your monitoring actually shows you. The two numbers can point in completely opposite directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;The metric almost nobody has on a dashboard is throttling, not usage. On the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/cpu.stat
&lt;span class="c"&gt;# nr_periods 48213&lt;/span&gt;
&lt;span class="c"&gt;# nr_throttled 9104&lt;/span&gt;
&lt;span class="c"&gt;# throttled_usec 182004112&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nr_throttled&lt;/code&gt; divided by &lt;code&gt;nr_periods&lt;/code&gt; is your throttling ratio. Ours was north of 18% — meaning nearly one in five scheduling periods, the container was frozen mid-request. If you're on Kubernetes with cAdvisor/Prometheus, the same signal is &lt;code&gt;container_cpu_cfs_throttled_periods_total&lt;/code&gt; and &lt;code&gt;container_cpu_cfs_throttled_seconds_total&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rate(container_cpu_cfs_throttled_periods_total[5m])
  / rate(container_cpu_cfs_periods_total[5m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything consistently above ~5% is worth investigating; above 20-25%, it's very likely a real contributor to tail latency, not a coincidence.&lt;/p&gt;

&lt;p&gt;Once you can see it, the fixes are unglamorous but effective, roughly in order of how fast they help:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raise the CPU limit&lt;/strong&gt; (or remove it and rely on requests + node-level capacity). Immediate relief, costs more compute headroom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right-size your concurrency.&lt;/strong&gt; If your runtime spins up more worker threads than your quota can actually sustain in a 100ms burst, you're manufacturing your own throttling. Cap thread/worker pool size to something the quota can plausibly cover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the ratio, not the average.&lt;/strong&gt; Add the throttling metric to the same dashboard as CPU usage so the two numbers can't tell different stories unchallenged.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We ended up doing both 1 and 2 — a modest limit bump plus trimming an overeager thread pool that had been sized for a machine, not a slice of one. The throttling ratio dropped from ~18% to under 1%, and the "random" p99 spikes went with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CPU limit is a hard budget per 100ms accounting period, not a cap on the average your dashboard shows you.&lt;/li&gt;
&lt;li&gt;Low average CPU usage does not rule out throttling — bursty workloads can be throttled constantly while looking idle on a 1-minute chart.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;nr_throttled&lt;/code&gt; in &lt;code&gt;cpu.stat&lt;/code&gt; (or &lt;code&gt;container_cpu_cfs_throttled_seconds_total&lt;/code&gt; in Prometheus) directly — it's a different signal than usage, and most teams never graph it.&lt;/li&gt;
&lt;li&gt;If concurrency inside a container regularly exceeds what its quota can sustain in one period, you're throttling yourself before any external load shows up.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Chaining Five Fast Services Gives You One Slow One</title>
      <dc:creator>speed engineer</dc:creator>
      <pubDate>Tue, 28 Jul 2026 04:48:41 +0000</pubDate>
      <link>https://dev.to/speed_engineer/why-chaining-five-fast-services-gives-you-one-slow-one-3hj3</link>
      <guid>https://dev.to/speed_engineer/why-chaining-five-fast-services-gives-you-one-slow-one-3hj3</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Five services, each with a P99 latency of 200ms and a comfortable P50 of 20ms. Every team's dashboard is green. Every individual SLO is met. And yet the end-to-end request that touches all five has a P99 well over a second, and nobody can find the "slow service" causing it — because on any given slow request, a different service is the culprit.&lt;/p&gt;

&lt;p&gt;This is the conversation that repeats in incident review after incident review: "It's not us, our P99 is 200ms, look at the graph." Five teams, five graphs, five clean dashboards, one furious product owner staring at a checkout flow that blows its SLA on 1 request in 20.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Percentiles don't add. That's the part almost nobody's intuition gets right on the first try.&lt;/p&gt;

&lt;p&gt;If five services are called in sequence and each is independently slow (above its own P99 threshold) 1% of the time, the probability that the whole chain stays fast — every single hop landing below its own P99 — is 0.99 raised to the fifth power, which is about 95.1%. Flip it around: roughly 4.9% of end-to-end requests will hit at least one hop having a bad day. What was a 1-in-100 event for any single service becomes something close to a 1-in-20 event for the request your user actually experiences. You didn't add latency. You added &lt;em&gt;opportunities&lt;/em&gt; to be unlucky, and tail latency is entirely about how often you get unlucky.&lt;/p&gt;

&lt;p&gt;Fan-out makes this worse, not better. A sequential chain waits on one slow link; a scatter-gather call — hitting N shards or N replicas in parallel and waiting for all of them — waits on the &lt;em&gt;slowest&lt;/em&gt; of N independent draws from the same distribution. Jeff Dean and Luiz André Barroso's 2013 paper "The Tail at Scale" has the sharpest framing of this I've seen: at large fan-out (hundreds of parallel calls, common in search or ad-serving backends), the probability that at least one call lands in the tail approaches 1. Almost every request ends up waiting on somebody's P99, even though no individual server is slow very often.&lt;/p&gt;

&lt;p&gt;Two things quietly make this worse in real systems: hops usually aren't fully independent (a shared resource — a connection pool, a downlink, a noisy-neighbor node — correlates slowness across calls that "shouldn't" be related), and a single very slow hop (a GC pause, a retry) can dominate the whole chain's latency by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Measure the critical path, not per-service dashboards.&lt;/strong&gt; Distributed tracing (OpenTelemetry span data is enough) shows you which hop is actually on the critical path for slow requests — not which service has the worst P99 in isolation, which is usually a different, less useful ranking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use hedged (backup) requests for latency-critical fan-out.&lt;/strong&gt; Dean and Barroso's mitigation, and it's simple: if a request hasn't returned within, say, the 95th-percentile expected time, fire a second, identical request to a different replica and take whichever comes back first, cancelling the loser. This trades a small amount of duplicate work for a large cut in tail latency, because now &lt;em&gt;both&lt;/em&gt; attempts have to be slow, not just one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set per-hop deadlines that shrink as they propagate.&lt;/strong&gt; A 2-second end-to-end budget split across five sequential hops shouldn't give each hop 2 seconds to retry in — propagate a shrinking deadline so a hop that's already burned 1.5 seconds doesn't get to spend another 2 trying again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduce fan-out width before you optimize per-node latency.&lt;/strong&gt; Cutting a scatter-gather from 50 shards to 10 often does more for tail latency than shaving 20ms off each shard's P99, because you're directly attacking the exponent, not the base.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget for P99, not P50, at every hop you add.&lt;/strong&gt; Each additional synchronous hop taxes your tail latency measurably even if it barely moves your average — that tax is invisible until you do the math above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Percentiles don't add across hops — probabilities of badness compound, and tail latency is a probability-of-badness problem.&lt;/li&gt;
&lt;li&gt;A chain of N services, each bad p% of the time independently, fails end-to-end at roughly &lt;code&gt;1 - (1-p)^N&lt;/code&gt; of requests — far more often than any single hop's own SLO suggests.&lt;/li&gt;
&lt;li&gt;Parallel fan-out is worse than sequential chaining for tail latency: you wait on the slowest of N draws, not just one.&lt;/li&gt;
&lt;li&gt;Hedged/backup requests, shrinking per-hop deadlines, and narrower fan-out all directly attack this multiplication; shaving per-service P50 mostly doesn't.&lt;/li&gt;
&lt;li&gt;Trace the critical path per slow request instead of trusting per-service dashboards — they can all be green while the request isn't.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
