RORK LABJP
EVENT — Apple holds its Surprise and Shine event today, September 9, starting at 10:00 Pacific. That lands in the small hours of September 10 in JapanEXPECT — Expected are the iPhone 18 Pro and Pro Max, a foldable, the 2nm A20 Pro chip, and release dates for iOS 27 and its sibling updatesWAIT — As this is written the event has not happened yet. Rumor-stage writing and post-announcement writing look identical once they are mixed togetherMAX — Since Rork Max generates native Swift, Apple news is not somebody else's problem. Worth repeating that the standard product still writes React NativeSIMULATOR — Rork Max compiles on cloud Macs and lets you check the result in a streaming iOS simulator inside the browser, with no Xcode and no Mac hardwareSEASON — A new OS is when automated build pipelines wobble most. An article selling convenience owes its readers a word about that wobbleEVENT — Apple holds its Surprise and Shine event today, September 9, starting at 10:00 Pacific. That lands in the small hours of September 10 in JapanEXPECT — Expected are the iPhone 18 Pro and Pro Max, a foldable, the 2nm A20 Pro chip, and release dates for iOS 27 and its sibling updatesWAIT — As this is written the event has not happened yet. Rumor-stage writing and post-announcement writing look identical once they are mixed togetherMAX — Since Rork Max generates native Swift, Apple news is not somebody else's problem. Worth repeating that the standard product still writes React NativeSIMULATOR — Rork Max compiles on cloud Macs and lets you check the result in a streaming iOS simulator inside the browser, with no Xcode and no Mac hardwareSEASON — A new OS is when automated build pipelines wobble most. An article selling convenience owes its readers a word about that wobble
Articles/Dev Tools
Dev Tools/2026-08-08Advanced

The 429s Kept Coming After Recovery: Measuring Retry Jitter Across 2,000 Clients

My backend came back but the 429s did not stop. I rebuilt the scenario in a 2,000-client discrete simulator and measured what plain exponential backoff, delay caps, 10% jitter, and honoring Retry-After actually cost in seconds.

retrybackoffExpo203React Native236reliability

Premium Article

One morning I took down the backend behind my indie developer apps for about four minutes. It was supposed to be a one-line config change; the deploy failed and left the service dead.

I rolled it back and watched the logs, relieved. The relief did not last. The service was up, capacity was back, and the 429s kept coming.

Opening the app on my own phone gave me the same thing. Errors, for several minutes, against a server that was demonstrably healthy.

The cause was in my own retry code. Worse, the part doing the most damage was the part I had written by the book.

I could not reason my way out of it, so I built a small simulator that replays 2,000 clients reconnecting, and measured what each implementation actually costs in seconds.

What was actually happening

When an outage hits, every client with the app open fails at roughly the same instant. Because their failures are aligned, their retries are aligned too.

The moment the server recovers, that aligned population arrives together. Capacity is exceeded, so the server returns 429. The clients that got 429 come back — still aligned.

The server had recovered. My own clients were keeping it busy.

That much is the standard thundering herd story. What I did not have was the quantitative half: which mitigation buys how much, and where the surprises are.

Building the test rig

Experimenting on a live service was not an option, so I wrote a discrete-time simulator. One tick is 100ms, and the loop only models three things: clients sending, the server accepting up to capacity, and rejected clients rescheduling.

The parameters mirror my setup. 2,000 clients, recovery at the 30 second mark, 120 req/sec of post-recovery capacity. Randomness comes from a seeded mulberry32, and every figure below is the mean of five seeds.

// herd.mjs — run with: node herd.mjs
const N = 2000;               // clients that failed together
const RECOVER_MS = 30000;     // when the server comes back
const CAPACITY_PER_SEC = 120; // post-recovery capacity
const TICK = 100;             // one tick = 100ms
const CAP_TICK = CAPACITY_PER_SEC * TICK / 1000;
const BASE = 1000;            // base delay, 1s
 
// Seeded PRNG so results are reproducible
function mulberry32(a) {
  return function () {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
 
function run(nextDelay, seed) {
  const rand = mulberry32(seed);
  const next = new Float64Array(N);   // next send time per client
  const att  = new Int32Array(N);     // attempt count
  const prev = new Float64Array(N);   // previous delay (for decorrelated jitter)
  const done = new Uint8Array(N);
  for (let i = 0; i < N; i++) { next[i] = rand() * 2000; prev[i] = BASE; }
 
  let sentAfterRecovery = 0, ok = 0, peak = 0, t99 = -1;
 
  for (let t = 0; t < 1200000; t += TICK) {
    const arrivals = [];
    for (let i = 0; i < N; i++) if (!done[i] && next[i] <= t) arrivals.push(i);
 
    const up = t >= RECOVER_MS;
    if (up) {
      sentAfterRecovery += arrivals.length;
      const qps = arrivals.length * 1000 / TICK;
      if (qps > peak) peak = qps;
    }
 
    // Everything fails before recovery; afterwards, anything over capacity gets a 429
    let budget = up ? CAP_TICK : 0;
    for (const i of arrivals) {
      if (budget >= 1) { budget--; done[i] = 1; ok++; }
      else {
        att[i]++;
        const d = nextDelay(att[i], prev[i], rand);
        prev[i] = d;
        next[i] = t + d;
      }
    }
    if (t99 < 0 && ok >= N * 0.99) t99 = t;
    if (ok === N) break;
  }
  return { sentAfterRecovery, ok, peak: Math.round(peak), t99 };
}

I track three numbers: total requests reaching the server after recovery, peak post-recovery QPS, and the moment 99% of clients have gotten through.

Mean latency would be misleading here. What people experience during an outage is when the slowest slice comes back.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Measured numbers showing exponential backoff without jitter finishing 7x slower than a naive fixed 5s interval
Why the implementation that honors Retry-After exactly was the slowest of all, and how to rewrite it as a floor
A drop-in fetch wrapper combining decorrelated jitter with a per-install offset, with smoke test output
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
Dev Tools2026-09-01
Adding image paste with expo-paste-input, and moving the disappearing file:// URIs out of cache
How to let users paste images and GIFs into a chat input with expo-paste-input. The URIs that onPaste hands you are temporary files that can vanish before the user hits send. Here is the relocation code and the order I verify it on real devices.
Dev Tools2026-08-31
Three Minutes After the Screen Locked, My expo-audio Ambient Sound Went Silent
Why expo-audio stops playing when the screen locks, diagnosed as three separate layers: build config, audio session, and lock screen integration. The three-minute stop on Android is documented behavior.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links