Jitter for Retries — Complete Implementation Guide
In this tutorial, you will learn about Jitter for Retries. We cover key concepts, practical examples, and best practices to help you master this topic.
Jitter adds randomness to retry delays, preventing the thundering herd problem where all clients retry simultaneously and overwhelm the recovering service.
What You'll Learn
By the end of this tutorial, you will implement three jitter strategies, understand when to use each, and prevent synchronized retry storms in Distributed Systems.
Why It Matters
Without jitter, exponential backoff synchronizes across clients. When a service recovers, all clients retry at once, creating a new failure spike. DodaTech uses jitter to spread retry load.
Real-World Use
Doda Browser's 10 million users sync bookmarks daily. When the sync service restarts, jittered retries spread the reconnection load over 30 seconds instead of all hitting at once.
Jitter Learning Path
flowchart LR
A[Exponential Backoff] --> B[Jitter]
B --> C[Full Jitter]
B --> D[Equal Jitter]
B --> E[Decorrelated Jitter]
B --> F{You Are Here}
style F fill:#f90,color:#fff
The Thundering Herd Problem
Without jitter, all clients using the same backoff schedule retry at identical times, creating synchronized load spikes.
// All clients retry at exactly 200ms, 400ms, 800ms, etc.
// After a service restart, 10,000 clients all retry at 200ms
// This creates 10,000 concurrent requests - a new failure
// Solution: add randomness so retries spread out
Full Jitter
Full jitter randomizes the delay between 0 and the calculated exponential backoff value, providing the best load distribution.
function fullJitter(baseDelay, attempt, maxDelay) {
const exponential = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
return Math.random() * exponential;
}
async function retryWithFullJitter(fn, options = {}) {
const baseDelay = options.baseDelay || 200;
const maxDelay = options.maxDelay || 30000;
const maxRetries = options.maxRetries || 5;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
const delay = fullJitter(baseDelay, attempt, maxDelay);
console.log(`Full jitter: waiting ${Math.round(delay)}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
}
// Delays range: 0-200ms, 0-400ms, 0-800ms, 0-1600ms, 0-3200ms
Equal Jitter
Equal jitter splits the exponential delay into two parts: half is a guaranteed wait, half is random. This ensures at least some waiting.
function equalJitter(baseDelay, attempt, maxDelay) {
const exponential = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const half = exponential / 2;
return half + Math.random() * half;
}
async function retryWithEqualJitter(fn, options = {}) {
const baseDelay = options.baseDelay || 200;
const maxDelay = options.maxDelay || 30000;
const maxRetries = options.maxRetries || 5;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
const delay = equalJitter(baseDelay, attempt, maxDelay);
console.log(`Equal jitter: waiting ${Math.round(delay)}ms (min ${Math.round(delay / 2)})`);
await new Promise(r => setTimeout(r, delay));
}
}
}
// Delays range: 100-200ms, 200-400ms, 400-800ms, etc.
// At least half the exponential value is guaranteed
Decorrelated Jitter
Decorrelated jitter uses the previous delay to calculate the next, creating a smoother distribution of retry times.
function decorrelatedJitter(previousDelay, baseDelay, maxDelay) {
const min = baseDelay;
const max = Math.min(previousDelay * 3, maxDelay);
return min + Math.random() * (max - min);
}
async function retryWithDecorrelatedJitter(fn, options = {}) {
const baseDelay = options.baseDelay || 200;
const maxDelay = options.maxDelay || 30000;
const maxRetries = options.maxRetries || 5;
let previousDelay = baseDelay;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
const delay = decorrelatedJitter(previousDelay, baseDelay, maxDelay);
previousDelay = delay;
console.log(`Decorrelated jitter: waiting ${Math.round(delay)}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
}
Jitter Strategy Comparison
| Strategy | Spread | Guaranteed Delay | Best For |
|---|---|---|---|
| None | None | Full delay | Development |
| Full | Maximum | None | Most distributed systems |
| Equal | Good | Half delay | User-facing operations |
| Decorrelated | Very good | Base delay | High-traffic systems |
Common Mistakes
Using no jitter in production -- Without jitter, multiple clients create synchronized retry storms. Always add jitter.
Full jitter with zero delays -- Full jitter can return 0ms delay, causing immediate retry. Set a minimum delay of 50-100ms.
Not considering client count -- More clients need more jitter spread. Increase max delay for systems with many concurrent clients.
Using jitter with fixed backoff -- Jitter on fixed backoff still creates partial synchronization. Use exponential base.
Not seeding random generator -- Some environments use predictable random seeds. Use crypto.randomBytes for high-security applications.
Practice Questions
What problem does jitter solve in retry strategies? The thundering herd problem where all clients retry at the same time, overwhelming the recovering service.
What is the difference between full jitter and equal jitter? Full jitter ranges from 0 to exponential value. Equal jitter ranges from half to full exponential value.
When should you use decorrelated jitter? In high-traffic distributed systems where you want smoother load distribution over time.
Challenge: Implement a minimum delay for full jitter to prevent instant retries.
function fullJitterWithMin(baseDelay, attempt, maxDelay, minDelay = 50) {
const exponential = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
return minDelay + Math.random() * (exponential - minDelay);
}
FAQ
Mini Project
Build a jitter strategy selector that implements all three jitter types and measures their retry distribution.
class JitterStrategy {
static full(baseDelay, attempt, maxDelay) {
const exp = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
return Math.random() * exp;
}
static equal(baseDelay, attempt, maxDelay) {
const exp = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const half = exp / 2;
return half + Math.random() * half;
}
static decorrelated(prevDelay, baseDelay, maxDelay) {
const min = baseDelay;
const max = Math.min(prevDelay * 3, maxDelay);
return min + Math.random() * (max - min);
}
static create(type, options) {
return (attempt, prevDelay) => {
switch (type) {
case "full": return this.full(options.baseDelay, attempt, options.maxDelay);
case "equal": return this.equal(options.baseDelay, attempt, options.maxDelay);
case "decorrelated": return this.decorrelated(prevDelay, options.baseDelay, options.maxDelay);
default: return Math.min(options.baseDelay * Math.pow(2, attempt), options.maxDelay);
}
};
}
}
const strategy = JitterStrategy.create("full", { baseDelay: 200, maxDelay: 30000 });
const delays = Array.from({ length: 10 }, (_, i) => Math.round(strategy(i, 0)));
console.log("Sample delays (ms):", delays);
What's Next
Now that you understand jitter, explore combining retries with circuit breakers. Then learn about making operations safe for retries.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro