Half-Open Probes — Complete Implementation Guide
In this tutorial, you will learn about Half. We cover key concepts, practical examples, and best practices to help you master this topic.
Half-open probes test whether a recovering service is ready to accept traffic, sending limited requests while the circuit is in a transitional state between open and closed.
What You'll Learn
By the end of this tutorial, you will implement half-open probe strategies, configure probe frequency and success thresholds, and handle probe failures safely.
Why It Matters
The half-open state is the most delicate part of circuit breaker design. Too aggressive probes overwhelm a recovering service. Too conservative probes delay recovery.
Real-World Use
DodaTech's circuit breakers use single-probe half-open with a 30-second reset timeout. A single successful probe closes the circuit; a single failure reopens it.
Half-Open Learning Path
flowchart LR
A[Thresholds] --> B[Half-Open Probes]
B --> C[Single Probe]
B --> D[Multi-Probe]
B --> E[Graduated]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Single Probe Strategy
The simplest half-open strategy: send one request. If it succeeds, close the circuit. If it fails, reopen.
class SingleProbeCircuitBreaker {
constructor(options = {}) {
this.threshold = options.threshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.state = "closed";
this.failureCount = 0;
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) {
throw new Error("Circuit open");
}
this.state = "half-open";
}
try {
const result = await fn();
if (this.state === "half-open") {
this.state = "closed";
this.failureCount = 0;
}
return result;
} catch (err) {
this.failureCount++;
if (this.state === "half-open" || this.failureCount >= this.threshold) {
this.state = "open";
this.nextAttempt = Date.now() + this.resetTimeout;
}
throw err;
}
}
}
Multi-Probe Strategy
Send multiple probe requests to get a statistically significant sample of the service's health.
class MultiProbeBreaker {
constructor(options = {}) {
this.probeCount = options.probeCount || 3;
this.successThreshold = options.successThreshold || 0.66;
this.probeTimeout = options.probeTimeout || 5000;
this.state = "closed";
this.failureCount = 0;
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) throw new Error("Open");
this.state = "half-open";
}
if (this.state === "half-open") {
return this.probe(fn);
}
return this.normalCall(fn);
}
async probe(fn) {
const results = [];
for (let i = 0; i < this.probeCount; i++) {
try {
const result = await Promise.race([
fn(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("probe_timeout")), this.probeTimeout)
)
]);
results.push({ success: true, result });
} catch {
results.push({ success: false });
}
}
const successes = results.filter(r => r.success).length;
const successRate = successes / this.probeCount;
if (successRate >= this.successThreshold) {
this.state = "closed";
this.failureCount = 0;
return results.find(r => r.success).result;
}
this.state = "open";
this.nextAttempt = Date.now() + this.resetTimeout;
throw new Error(`Probe failed: ${successes}/${this.probeCount} successful`);
}
async normalCall(fn) {
try {
const result = await fn();
return result;
} catch (err) {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = "open";
this.nextAttempt = Date.now() + this.resetTimeout;
}
throw err;
}
}
}
Graduated Probe
Start with a single probe request and gradually increase probe traffic as the service shows signs of recovery.
class GraduatedProbeBreaker {
constructor() {
this.state = "closed";
this.consecutiveHalfOpenFailures = 0;
this.maxProbes = 1;
}
getProbeCount() {
return Math.min(this.maxProbes, 10);
}
async call(fn) {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) throw new Error("Open");
this.state = "half-open";
}
if (this.state === "half-open") {
const probes = this.getProbeCount();
let successes = 0;
for (let i = 0; i < probes; i++) {
try {
await fn();
successes++;
} catch {}
}
if (successes === probes) {
this.state = "closed";
this.consecutiveHalfOpenFailures = 0;
this.maxProbes = 1;
return;
}
this.consecutiveHalfOpenFailures++;
this.maxProbes = Math.min(this.maxProbes + 1, 10);
this.state = "open";
this.nextAttempt = Date.now() + this.resetTimeout * Math.pow(2, this.consecutiveHalfOpenFailures);
throw new Error(`Graduated probe: ${successes}/${probes} succeeded`);
}
// Normal closed handling
try {
return await fn();
} catch (err) {
this.failureCount++;
if (this.failureCount >= 5) {
this.state = "open";
this.nextAttempt = Date.now() + 30000;
}
throw err;
}
}
}
Common Mistakes
Sending full traffic in half-open -- The half-open state should send minimal traffic. A single probe is usually sufficient.
No timeout on probes -- A hanging probe keeps the circuit half-open indefinitely. Always set probe timeouts.
Ignoring probe errors -- If half-open probes fail, immediately reopen. Do not mix probe failures with the closed counter.
Probing too frequently -- After a failed probe, wait at least the reset timeout before probing again.
Not logging probe results -- Probe successes and failures are important signals for operations teams.
Practice Questions
Why use multiple probes instead of a single probe? Multiple probes provide statistical confidence. A single probe might succeed by luck even if the service is unhealthy.
What happens if a half-open probe times out? The circuit should treat a timeout as a failure and reopen immediately.
How does graduated probing help recovering services? It starts with minimal load and increases gradually as the service demonstrates stability.
Challenge: Implement a half-open strategy that adapts probe count based on failure history.
class AdaptiveProbeBreaker {
getProbeCount() {
if (this.consecutiveFailures < 2) return 1;
if (this.consecutiveFailures < 5) return 3;
return 5;
}
}
FAQ
Mini Project
Build a half-open probe system with multiple strategies, configurable probe counts, and adaptive behavior.
class ProbeStrategy {
static single() {
return { type: "single", probeCount: 1, successThreshold: 1 };
}
static multi(count = 3) {
return { type: "multi", probeCount: count, successThreshold: Math.ceil(count * 0.66) };
}
static graduated() {
return { type: "graduated", initialProbes: 1, maxProbes: 10 };
}
static execute(strategy, fn) {
if (strategy.type === "single") {
return strategy.singleProbe(fn);
}
if (strategy.type === "multi") {
return strategy.multiProbe(fn);
}
return strategy.graduatedProbe(fn);
}
async singleProbe(fn) {
await fn();
}
async multiProbe(fn) {
let successes = 0;
for (let i = 0; i < this.probeCount; i++) {
try { await fn(); successes++; } catch {}
}
if (successes < this.successThreshold) {
throw new Error(`Probe failed: ${successes}/${this.probeCount}`);
}
}
async graduatedProbe(fn) {
// Implementation with increasing probe count
}
}
What's Next
Now that you understand half-open probes, explore monitoring circuit breaker health. Then learn about using circuit breakers for HTTP calls.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro