who broke checkout?
try to find which service broke! :)
what this is
faultline replays a microservice incident like a video: metrics, traces, logs and deployments on one event-time clock, with every service scored for root cause as the evidence lands.
the problem it mocks
a modern web app isn't one program. it's dozens of small services calling each other: the storefront calls checkout, checkout calls recommendations, recommendations call the catalog. when one of them goes wrong the symptoms show up everywhere at once, because everything depends on everything, and errors spike three services away from the one that actually broke.
working backwards from those symptoms to the service that started it is called root cause analysis, and in real life it's a human at 3am staring at dashboards and guessing. the dashboards don't help as much as they should, because they show you now, and what you need is the order things happened in. the tools that do show you the order sell it as a black box, and lately the box has an llm in it. faultline does the working-backwards part in the open: you can rewind, you can see every piece of evidence, and you can see exactly how it got a case wrong.
here's the shop the real cases come from, google's online boutique demo, drawn as who calls whom, with one ordinary failure in it.
the leak is in one box, the symptoms are in four, and the alert names the one furthest from the cause, because the frontend is the only service a customer can see. the eight healthy ones still show up on every dashboard, and at 3am they all look about the same.
how it works
every metric sample, log line, trace span, and deployment carries the time it happened, not the time it arrived. faultline sorts the entire incident onto that one clock and replays it, which is what lets you scrub backwards.
as the clock moves, each service gets a rolling baseline of its own normal, a median and a spread, and anything that strays far enough from it is flagged. at the end, nine kinds of evidence go into a fixed weighted sum for every service: how far it strayed, whether it went wrong before the services that call it, whether failed requests route through it, whether a deploy landed just before, and so on. that fixed sum is the whole trick, because same input means same score to the byte, and every score comes apart into the nine numbers that made it, so you can argue with any of them.
the other word worth knowing is watermark, which is the engine's promise that it has seen everything up to a given moment. telemetry arrives late and out of order, so the promise leaves a two second allowance, and anything that turns up later than that is counted but not trusted.
how an event gets in
there's really only one loop in the whole engine, and everything you see on the page falls out of it. here's a single event making the trip from disk to a pixel.
step 2 is the one that took me longest to respect. a metric that shows up late isn't wrong, it's late, so a window that already closed gets one more second to change its mind, and the ui draws the revision rather than pretending the first answer was final.
step 3 is why the percentiles are approximate. an exact p99 means keeping every sample sorted, which doesn't fit in a stream, so each window keeps a ddsketch instead, a set of logarithmic buckets that answers within one percent. the heatmap, the sql engine, and the benchmarks share that one sketch, so a query and a pixel provably agree.
what a verdict looks like
this is the guided incident in the box above, one row per moment.
the crossed dots are services that strayed from their own normal, the dashed halo is a deploy that landed on one, and the filled dot is the one faultline ranks first. left to right they're frontend, checkout, recommendation, cart, and catalog, and the two that never get flagged are the ones nobody on the failing path calls.
the same fifteen seconds as the engine's own numbers, straight from the heatmap:
the thing to notice is the two second gap between the black line leaving 40 and the grey ones following. that gap is the whole case for replay: it's the evidence that recommendation went first, and it only exists on the event-time clock. a dashboard opened at second twelve shows three high lines and no order.
the weights are a starting hypothesis, not a tuned optimum: anomaly strength gets a fifth, three features get fifteen percent, and the rest ten or five. they're fixed on purpose, because tuning them on the cases i'm about to report would make the report worthless.
how the pieces fit together
the engine is only part of the system, so the diagram below is worth reading in two halves. the top half is everything sitting behind the box above, and it shows how a click you make actually reaches the engine. the bottom half is what three of those clicks do once they land, which is where the streaming machinery stops being plumbing and starts being the point.
the app you're clicking is a bundle built from the faultline repo and shipped with this wiki. everything it talks to is on the other side of the tunnel, and it talks to it directly, because vercel can't proxy a websocket.
the third box is the one i care about most. in every incident, real or synthetic, the injected fault is written down in a labels file, and the api only hands it out when a request explicitly asks for it. the ranker has no such request in it, and the benchmark that produces the numbers below runs with the same gate closed.
how i built it
i started at the bottom, with a canonical envelope for every kind of event and a replay clock that reads them off parquet in event-time order, because nothing above that layer means much if two runs of the same incident can disagree. the engine came next: watermarks per partition, tumbling and hopping windows that can revise themselves, the ddsketch percentile, and a left interval join that lines deployments up against the anomalies near them.
then the parts a recruiter can see: the ranking, the trace critical path, checkpoints, and a small sql planner, because a query is a ui too. the planner takes a subset of sql, lowers it through nine logical nodes and six rewrite rules, and runs it on the same operators the heatmap uses, which is why a p99 from the sql box and the p99 in the heatmap are the same number.
all of that worked on my fixtures, and then i loaded a real case and made my first mistake visible: the streaming heatmap rebuilds itself from every event on every publish. on a fifteen second synthetic incident that's nothing. on a real case with 155 thousand events it wedged the server for about seventy seconds per publish, so for now anything past twenty thousand events loads precomputed and the streaming toggle politely refuses. the incremental rebuild is the real fix, and i haven't built it.
the second mistake was really four, and i found them all in one afternoon by running the browser tests against the server under load instead of one at a time. sessions whose tab had closed were never evicted. playback kept running for sessions nobody was watching. the watermark metrics were recomputed on every single event, walking every partition through a string-keyed map, which is invisible on a few hundred events and ruinous on 155 thousand. and the size gate above didn't exist yet. all four are fixed.
the third mistake is the one that embarrasses me most, because it wasn't a bug in the engine, it was a number i believed. my benchmark harness reported batching speedups of six to fifteen times, and i put them in the readme and on this page. then i read the harness properly: it took two clock readings and pushed to a vector inside the timed loop, so at batch size one it was mostly timing itself, and two of the workloads never touched an engine operator at all. that table is withdrawn, and no throughput number gets claimed until the harness measures the real thing.
the fourth mistake arrived when i ran the benchmark's baselines myself, on the exact same real cases, and lost to the simplest one. that story gets its own section below, because it's the most useful thing on this page.
that's four mistakes in all, and the two that hurt most weren't bugs. they were numbers i'd said out loud before checking.
what's in the box
the box at the top is a recorded incident, and the engine replaying it is the real one, running on a real server. press play and the clock moves at ten times real time: a deploy lands, one service's memory climbs, and the services that call it slow down a few seconds later. the number next to each service is faultline's score for it being the cause, and the answer stays hidden until you press reveal, so you get to guess first.
the picker holds six incidents. one is the guided one, one is from my synthetic test suite, and four are real, from a public benchmark that injects faults into a demo shop running on a real cluster. faultline gets two of the real ones right, misses one by a single rank, and misses the last one badly. those two are in there on purpose, and the next section is why.
what real data did to my numbers
on my 16-incident synthetic suite the ranking is perfect: 100% top-1, robust to every single-feature ablation. satisfying, and nearly meaningless, because i generated those faults, so of course they're separable. that suite is a smoke test of the pipeline, not evidence of anything about the world.
so i took 15 real cases from rcaeval, a public benchmark that injects faults into google's online boutique demo on a real cluster and records the metrics, logs, and traces. i used the cpu, memory, and network delay faults, one per service, and ran the same untuned pipeline blind. then i ran two methods that ship in the benchmark's own code on the same 15 cases, with the same service-level scoring: nsigma, the simplest baseline in there, and baro, a published method. one difference worth knowing: the benchmark hands its methods the moment the fault was injected, so they compare before with after, while faultline has to find the onset itself. this is the table:
| method | what it is | top-1 | top-3 | avg@5 |
|---|---|---|---|---|
| nsigma | a plain z-score threshold on the metrics | 93.3% | at least 93.3% | 0.96 |
| baro | a published bayesian change-point method | 13.3% | 93.3% | 0.76 |
| faultline | nine features, fixed weights, untuned | 26.7% | 46.7% | 0.41 |
a plain median-and-spread z-score finds the broken service 93% of the time where my whole evidence pipeline gets 27%. that's a loss, and it's a diagnostic one: faultline already computes that exact z-score as one of its nine features, so the other eight are burying the signal rather than sharpening it. the rank distribution agrees, because it's bimodal: seven cases land at rank 1 to 3, eight cases at rank 8 to 12, and almost nothing in between.
here's the hard miss from the picker, drawn the same way as the shop above, but with the graph the engine actually reconstructed from the real traces and the rank it gave every service.
compare it to the clean picture further up. there, one box was sick and three followed it. here, all twelve strayed from their own baseline inside the fault window, because a real shop under a real load test is noisy everywhere, and a cpu hog on a leaf service that sees about one call in three hundred barely moves the shape of anything. so the feature i weighted highest, anomaly strength, is a twelve-way tie, and the ranking gets decided by the small features and the penalty.
and the penalty is exactly backwards here. email is a leaf: checkout calls it, and nothing calls it after that. when the services that call it wobble a moment before it does, the contradiction rule reads that as the callers went wrong first, so this can't be email, and docks it the full tenth. that tenth is the entire distance between rank 1 and rank 12, which you can see when the two verdicts are taken apart side by side:
in the guided case the bar is long because six kinds of evidence agree, and the deploy sitting right before the fault adds its tenth on top. in the real case the bars are identical up to the third segment, and the winner is winning on a hair of topology. this is what the loss looks like from the inside.
the ablations say which features are doing real work. remove topology consistency and top-1 collapses from 26.7% to 6.7%; remove temporal precedence and it falls to 13.3%. remove change proximity and nothing moves, because rcaeval has no deployment events, so a feature i weighted at a tenth is structurally dead on every real case. the synthetic suite could never have told me that.
the fix is written down next to the numbers rather than built: score magnitude first, and use topology and precedence as tie-breakers instead of dominant weights, then re-run the same 15 cases. until that's done, no accuracy number goes on my rรฉsumรฉ, and this page reports the loss. publishing a loss feels bad for exactly one evening. shipping a number you can't defend feels worse forever.
what it does today
the engine numbers that survived the harness cleanup are the checkpoint ones, measured on my laptop and regenerated by a committed script that stamps the commit, machine, toolchain, seed, and dataset checksum.
| metric | p50 | p99 |
|---|---|---|
| checkpoint write, fsync included | 11.9 ms | 15.1 ms |
| recovery, read and validate | 0.42 ms | 0.76 ms |
| duplicate evidence after a forced crash | 0 | 0 |
the phrase for that is checkpoint recovery with idempotent projections, never exactly-once, because the second promises something about the world and the first only about my disk. a checkpoint is written manifest-last with a checksum per file, and the pointer to the newest one flips only after a fully valid directory exists, so a crash mid-write leaves the previous one untouched.
the demo server holds 24 sessions of fifteen minutes each, so if the box above says it's busy, that's the ceiling and not a crash.
what i'd still fix
- the reranking, before anything else about accuracy. magnitude first, topology and precedence as tie-breakers, same 15 cases, and the table above gets a fourth row whether it improves or not.
- then the other 75 real cases. rcaeval has 90 for this system, and 15 is enough to learn i'm losing but not enough to tune on honestly. that needs a train and test split, with both numbers reported.
- the streaming heatmap should update incrementally instead of rebuilding from every event. the twenty thousand event gate is a bandage.
- the benchmark harness needs to time whole passes over real operators. until it does, the engine has no throughput number, and i'd rather have none than a wrong one.
- the four real cases in the picker were converted on my laptop from a 4.2gb download, so a fresh clone gets only the synthetic ones.
learn more
the code's at faultline, laid out roughly like this:
crates/engineis the watermarks, windows, the ddsketch percentile, the temporal join, and operator snapshots.crates/inferenceis the baselines, the nine features, the fixed weights, and the evidence graph.crates/graphbuilds trace dags and pulls out the critical path.crates/stateis the manifest-last checkpoints and the corrupt-fallback recovery.crates/planneris the sql subset, the nine logical nodes, and explain.crates/apiis the sessions, the websocket, and the reveal gate.web/is the react app, built into the bundle you're clicking.python/faultline_datagenerates the synthetic incidents and converts the real ones.
if streaming or root cause analysis is new to you, this is the order i'd go in:
- press play in the box above, watch the scores move, then scrub back to the start and watch them empty.
- read streaming 101 and 102 by tyler akidau. event time and watermarks are the first half of this page, and he explains them better than i do.
- watch his talk below, which covers the same ground with pictures.
- read the ddsketch paper, which is short, and then the rcaeval paper, which is the dataset. their code runs the baselines i lost to in an afternoon.
- clone the repo, run
make demo, and readcrates/inference/src/ranking.rswith the table above open beside it.
if you liked this, the sibling project is come kill my cluster!, a raft key-value store written from scratch that you can break live from the page. same idea in both: build the machinery yourself, run it in public, and report what it actually does.