A node failing to reply might be due to a process crash, network partition, long CPU scheduling delays, or the observer itself being paused. Failure detection first describes what evidence the observer can obtain, and only then decides when to switch services.
In an asynchronous model where message delays have no upper bound, silence for a finite time does not prove that the other party has stopped. This does not mean systems cannot be built; rather, it requires separating "suspicion," "taking action," and "guaranteeing safety."
Heartbeats and Probing: Two Ways to Gather Evidence
Heartbeats are liveness signals actively sent by the observed node; probing is an active request for a response by the observer. Both can only prove that a specific path succeeded at a specific moment, not that all functions of the node are healthy.
Assume a node sends a heartbeat every 1 second, and the observer marks an anomaly after waiting 3 seconds. A shorter threshold detects real failures earlier but is more likely to misclassify brief pauses as failures; a longer threshold reduces false positives but extends the failover time. Thresholds should be combined with service goals, latency distributions, scheduling pauses, and observation costs, rather than being fixed to always be correct beyond a certain percentile.
Node A observing B's failure does not mean C observing B will necessarily fail. Failure detection results are observer-centric; broadcasting local failures directly as indisputable global facts amplifies local network jitter.
φ-accrual: Separating Anomaly Degree from Action Thresholds
Let the historical heartbeat interval be a random variable X, with cumulative distribution function F, and let t be the time elapsed since the last heartbeat. The suspicion metric can be defined as:
ϕ(t)=−log10(1−F(t))=−log10P(X>t)
If the estimated distribution suggests a tail probability of 0.01 that "it still needs to wait this long," then ϕ=2; if it is 0.00001, then ϕ=5. Larger numbers indicate that the current silence is more anomalous relative to historical behavior. It is not the "posterior probability that the node is dead"; the latter would require failure priors and a more complete statistical model.
The detector is responsible for outputting a continuous suspicion metric, while the upper layer selects thresholds based on different risk levels. For example, removing a load balancer candidate might be sensitive, while triggering expensive data reconstruction might require more evidence. This decoupling is more flexible than a single fixed alive/dead switch. Akka's φ detection documentation demonstrates implementation ideas based on historical intervals.
Distribution estimation also has boundaries: too few samples at startup, long-term load changes, multi-modal network latency, and observer GC pauses can all distort old statistics. The sampling window, minimum variance, startup grace period, and allowed heartbeat pauses are part of the model; one cannot simply copy a single φ threshold.
Why is one observer anxious while another waits, given the same 1200 ms silence?
Using a normal distribution to approximate historical intervals, the average interval is 1000 ms for both. For Link A, the standard deviation is 50 ms, so 1200 ms deviates from the mean by four standard deviations; for Link B, the standard deviation is 300 ms, so the same silence is less than one standard deviation. φ distinguishes the degree of anomaly in these two different contexts.
frommathimporterfc,sqrt,log10,isclose,infdefphi(elapsed,mean,stddev,pause_margin=0):ifelapsed<0orstddev<=0orpause_margin<0:raiseValueError("Interval and margin must be non-negative, and standard deviation must be positive")z=(elapsed-pause_margin-mean)/stddevtail=0.5*erfc(z/sqrt(2))return-log10(tail)iftailelseinfstable=phi(1200,1000,50)variable=phi(1200,1000,300)assert4.49<stable<4.51assert0.59<variable<0.61assertstable>2andvariable<2assertisclose(phi(1200,1000,50,pause_margin=200),log10(2))assertphi(1300,1000,50)>stable
Historical Model / Margin
Approximate φ Value
If Example Threshold is Set to 2
Mean 1000, Std Dev 50 ms
4.50
Trigger Suspicion
Mean 1000, Std Dev 300 ms
0.60
Continue Waiting
Std Dev 50, Additional Margin 200 ms
0.30
Continue Waiting
The code directly uses the complementary error function to calculate the tail, avoiding precision loss from calculating F close to 1 and then subtracting; extremely large intervals may still underflow to zero, in which case infinite suspicion is returned here. The normal distribution is only an approximation, and even assigns a small probability to negative intervals; it should not be treated as a physical law of the network.
This code also does not learn the sample window, set a minimum variance, or implement the startup phase; the threshold of 2 is only used for illustrative comparison, not as a deployment recommendation. Increasing the margin improves tolerance but also delays the detection of real failures; if the observer itself pauses, one cannot claim the problem is solved by modifying the statistics of remote nodes. The Akka documentation explains the impact of historical mean, variance, and acceptable pauses on the curve. φ Detector Documentation
Phi is a log tail probability, not death probability
The shaded tail is the historical model’s probability that the next heartbeat has not arrived. Keep silence at 1200 ms and widen the standard deviation: phi drops although the observation is unchanged. The reference distribution changes, not the peer’s actual health. With a fixed distribution, longer silence shrinks the tail and increases its negative logarithm. The plot is cropped; values use the full normal approximation.
Preparing the visual
Phi is a log tail probability, not death probability
The same silence is more unusual on a stable link. Phi=−log₁₀P(X>t): a tenfold smaller tail raises phi by one.
{"id":"phi-tail-probability","title":"Phi is a log tail probability, not death probability","summary":"The same silence is more unusual on a stable link. Phi=−log₁₀P(X>t): a tenfold smaller tail raises phi by one.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"silence\" data-i18n=\"silence\"></label><output for=\"silence\" id=\"silence-value\"></output><input id=\"silence\" type=\"range\" min=\"500\" max=\"1500\" value=\"1200\" step=\"10\"></div><div class=\"control\"><label for=\"sigma\" data-i18n=\"sigma\"></label><output for=\"sigma\" id=\"sigma-value\"></output><input id=\"sigma\" type=\"range\" min=\"50\" max=\"300\" value=\"50\" step=\"10\"></div><svg id=\"curve\" viewBox=\"0 0 400 150\" role=\"img\" data-i18n-label=\"tail\"><path id=\"area\" fill=\"var(--second-soft)\"/><path id=\"density\" fill=\"none\" stroke=\"var(--accent)\" stroke-width=\"2\"/><line id=\"marker\" y1=\"10\" y2=\"140\" stroke=\"var(--second)\" stroke-width=\"2\"/></svg><div class=\"axis\"><span>0 ms</span><span>1000</span><span>2000 ms</span></div><div class=\"metrics\"><div><span data-i18n=\"tail\"></span><strong id=\"tail\"></strong></div><div><span data-i18n=\"phi\"></span><strong id=\"phi\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"suspect\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"wait\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n#curve{width:100%;display:block}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function erfc(x){const z=Math.abs(x),t=1/(1+.5*z),r=t*Math.exp(-z*z-1.26551223+t*(1.00002368+t*(.37409196+t*(.09678418+t*(-.18628806+t*(.27886807+t*(-1.13520398+t*(1.48851587+t*(-.82215223+t*.17087277)))))))));return x>=0?r:2-r;}function draw(){const time=+$('#silence').value,sigma=+$('#sigma').value,p=.5*erfc((time-1000)/(sigma*Math.SQRT2)),phi=-Math.log10(p);set('silence-value',time);set('sigma-value',sigma);set('tail',p.toExponential(3));set('phi',phi.toFixed(3));set('equation','−log₁₀('+p.toExponential(3)+') = '+phi.toFixed(3));let points=[];for(let x=0;x<=2000;x+=5)points.push([x/5,140-110*50/sigma*Math.exp(-.5*((x-1000)/sigma)**2)]);$('#density').setAttribute('d','M'+points.map(p=>p.join(',')).join(' L'));const right=points.filter(p=>p[0]>=time/5);$('#area').setAttribute('d','M'+time/5+',140 L'+right.map(p=>p.join(',')).join(' L')+' L400,140 Z');$('#marker').setAttribute('x1',time/5);$('#marker').setAttribute('x2',time/5);set('explanation',t(phi>=2?'suspect':'wait'));}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"heading":"Phi is a log tail probability, not death probability","intro":"Mean interval is 1000 ms. Change silence duration and historical jitter; compare the right-tail area with phi.","phi":"φ","reset":"Reset","scope":"A normal approximation with mean 1000 ms. Tail values cover the full distribution; the plot shows 0–2000 ms. No sample learning; threshold 2 is illustrative.","scopeLabel":"Model scope and assumptions","sigma":"Historical standard deviation σ / ms","silence":"Silence t / ms","suspect":"Phi ≥ 2: illustrative suspicion threshold reached, not proof of death.","tail":"P(X > t)","wait":"Phi < 2: below the illustrative threshold, not a guarantee of health."}}
SWIM: Division of Labor Between Detection and Membership Propagation
SWIM uses random probing and, after direct probing fails, relies on other members to probe the target, reducing false positives caused by a single anomalous path. Indirect probing does not require all mediators to succeed; any valid ACK can provide evidence.
The suspicion phase gives live nodes the opportunity to refute outdated judgments, and then propagates the results via membership updates. Version information such as member incarnation is used to distinguish new liveness claims from old suspicions, preventing any old ACK from permanently overwriting newer states. SWIM Original Paper clearly separates failure detection from membership update propagation.
SWIM and φ are different designs: the former describes the probing and propagation protocol, while the latter describes the statistical suspicion calculation. They can be combined in some systems, but SWIM cannot be defined as "combining φ with gossip."
Full-mesh heartbeats send O(N) messages per node per round, totaling O(N2) for the whole group. Fixed probing fan-out can keep the number of probe messages per node at a constant order, but member lists, propagation payloads, retransmissions, and tail latency of failure discovery must still be accounted for separately; one cannot deduce that the entire system cost is constant from O(1) probing.
Who Has the Right to Take Over After Anomaly Detection
The detector saying "suspect the old master is unreachable" does not mean the old master has stopped. If the new master starts writing to the database, and the old master continues writing after recovering, there may be two operational sources simultaneously.
Safe takeover usually requires authority granted by a consensus or lease protocol, and requires the protected resource to check generation information such as fencing tokens. The detector accelerates the decision to suspect; the permission protocol prevents outdated instances from remaining effective. Even if the detector occasionally misjudges, the latter should protect safety.
This is consistent with the distinction in Fault Models: safety cannot rely solely on an empirical timeout that never misjudges.
Fencing must be checked on the resource side and occur atomically with the write
Assume the authorization service issues tokens 41 and 42 in sequence. The old master holds 41, pauses, and then recovers; the new master holds 42. The resource saves the maximum token it has seen and rejects smaller generations during writes. The example below uses sequential function calls to represent the atomic "compare generation and write," without implementing the authorization service or database transactions.
classResource:def__init__(self):self.highest=0self.value=Nonedefwrite(self,token,value):iftoken<self.highest:returnFalseself.highest, self.value=token, valuereturnTruer=Resource()assertr.write(41,"old")assertr.write(42,"new")assertnotr.write(41,"late-old")assertr.value=="new"assertr.write(42,"new-again")# Multiple operations allowed within the same authorization period
# A new token that has been issued but not yet reached the resource will not automatically change the resource's check state.
r2=Resource()assertr2.write(41,"old")issued_token=42assertr2.write(41,"late-but-before-fence")assertr2.write(issued_token,"new")assertnotr2.write(41,"now-rejected")
The second group of calls defines the guarantee: once the resource sees 42, 41 can no longer overwrite it; merely issuing 42 does not instantly notify all external resources. If the requirement is to reject any old writes immediately after authorization switches, the resource must participate in this switch or atomically verify the current authorization state; one cannot rely solely on the "maximum seen token." The write-side mechanism of fencing can be found in Martin Kleppmann's analysis of expired lock holders.
Real tokens must be issued monotonically by a trusted authorization protocol; resources cannot allow arbitrary clients to self-report a huge number. Resources must also persistently save the generation and place the check and business modification in the same atomic operation; if the lock is released before writing the value after the check passes, old and new requests can still interleave. For duplicate deductions within the same token, request ID deduplication is still needed: fencing isolates outdated generations, idempotency eliminates retries of the same operation, and the two do not replace each other.
Deadlines, Timeouts, and Recovery Budgets
A request has a total deadline, and each downstream operation can only use the remaining budget. If A has already spent 80 ms and the total budget is 100 ms, giving B an independent 100 ms timeout would break the caller's promise. After a timeout, cancellation must also be propagated to avoid expired work continuing to occupy resources.
Retries also consume budget and capacity. Exponential backoff and jitter can distribute retries, but they cannot replace rate limiting, idempotency, and overload protection. Member loss timeouts, RPC deadlines, and health check thresholds serve different decisions and should not share a single "global timeout constant."