Reading time · 8 minSeptember 14, 2026

How a Bank Finds Its Own Root Cause in 20 Seconds

We run a real six-service bank that moves money continuously. When we stopped its ledger, the first alert fired in 20 seconds and the diagnosis named the dead service by name — not because a model guessed, but because the evidence was gathered first and the model was only asked to read it.

Kaushik VaranasiKaushik Varanasi

Key takeaways

  • A dependency failure is detectable in seconds if you alert on something whose normal value is zero — root-span error rate and ERROR-log count both sit at exactly zero in a healthy system, so any movement is signal.
  • The hardest failure to see is silence: a crashed service stops emitting, and every query that looks for errors will miss it entirely because the dead service produces none.
  • Ranking incidents deterministically and asking the model only to explain them keeps the answer reproducible — two people looking at the same incident get the same ordering, every time.
  • An alert whose threshold sits below the system's normal operating level can never turn off, and therefore can never catch anything new.
  • Root-cause analysis is cheap when the expensive part is a database query: gathering the evidence for one incident took under four seconds across six services and millions of spans.

Most observability demos are performed on a toy. Three services, a synthetic error injected on cue, a dashboard that lights up. Nothing is really at stake, so nothing is really proven.

We wanted something that could actually be wrong. So we built a bank.

The Bank

It is six services across two machines, and it moves money the way a bank does — with an edge tier that faces customers and a core tier that owns the money.

Architecture of a six-service bank. An edge tier on host A contains the gateway, auth service and payments service. A core banking tier on host B contains fraud scoring, the ledger, the accounts service and Postgres. Arrows trace a transfer from the gateway through authentication and payments, out to fraud scoring and the ledger.
One transfer touches five services across two hosts. Trace context rides along on each outbound call, so the whole path reconstructs as a single trace.

A transfer arrives at the gateway, which opens the root span — the one that represents the customer's actual request. Auth verifies the session before any money is touched. Payments orchestrates the movement: it asks fraud for a score, and if that comes back clean it calls the ledger, which writes a double-entry record to Postgres. Accounts reads from the ledger to serve balances and statements.

Nothing here is mocked. The ledger genuinely balances, transfers genuinely get declined for insufficient funds, and the whole thing has been running under continuous load for weeks.

730spans per second, steady state — about 63 million a day from six services

That volume matters, because it is what makes root-cause analysis hard. When something breaks, the evidence is real but it is buried in millions of records that all look approximately like each other.

What one request looks like

Here is a single failing transfer, as the product renders it.

A trace waterfall for one transfer. The gateway root span, an intermediate gateway client span, and the payments service span are all marked failed. A fraud scoring span succeeded. The deepest payments client span failed and has no child span beneath it. Below, four correlated log lines from the same trace.
Depth is the diagnosis. The failure is marked at every level, but only the innermost span is where it actually happened.

Read it from the bottom. The gateway failed, and payments failed, and the gateway's client call failed — but those are all just the failure travelling upwards. The interesting row is the last one: a payments client span that failed in 4.2 milliseconds with no child span underneath it.

That absence is the whole story. A child span would have told us which service was called and what it said. There isn't one, which means the callee never got far enough to report anything.

Stopping the ledger

To see whether any of this works when it counts, we stopped the ledger service outright. No graceful shutdown, no draining — the process simply went away, the way processes do.

Within seconds the shape of the system changed:

SignalHealthyDuring
Root-span error rate0%37.7%
ERROR-level logs0 per hour5,283 in two minutes
Ledger spans13,630 per 5 min0

The alerts fired in this order:

AlertDetection time
Any ERROR or FATAL log20 seconds
Root-span error rate above 1%50 seconds
Warning-log spike4 min 20 s
Ledger service silent5 min 20 s

The two fast ones share a property worth stealing: their healthy value is exactly zero. A well-behaved system produces no ERROR logs and fails no customer requests, so any movement at all is signal. You do not need a clever threshold when the baseline is nothing.

The slow ones are slow for a structural reason. A rule that asks "has this service gone quiet?" looks at a five-minute window, and for the first five minutes that window still contains healthy traffic from before the incident. You cannot detect absence faster than your window. That rule is confirmation, not detection — and it is worth having anyway, because it is the one that states the problem in plain language.

The part that is not a chatbot

Now the interesting bit. An alert says a number moved. The engineer's next question is why, and answering it is a mechanical slog: work out which service, pull its logs, filter to errors, find a failing request, walk the span tree, compare against an hour ago.

All of that is database work. So we do the database work first.

A pipeline diagram. A firing alert leads to five evidence panels gathered automatically: blast radius, failing operations, log patterns, one reconstructed failing request, and which services went silent. These feed a final panel stating with high confidence that the ledger service stopped emitting spans, from 11,806 to zero.
Five queries, under four seconds. The model is handed the answers, not the question.

When an alert fires, five things are gathered before any model is involved:

  1. Blast radius — every service currently erroring, next to what it was doing before the alert.
  2. Failing operations — the exact endpoints throwing errors, ranked.
  3. Log patterns — millions of log lines grouped into a few dozen templates, each with its pre-incident volume, so a genuinely new message is obvious.
  4. A real failing request — one trace, rebuilt span by span, down to the innermost failure.
  5. Silence — which services have stopped emitting altogether.

That fifth one is the one people leave out, and it is the one that solved this incident.

Everything else in the list looks for errors. But a dead service does not error — it goes quiet. Its callers do the erroring, and all they can say is that something downstream failed to answer. Search the telemetry for exceptions and you will describe the symptom perfectly and never name the cause.

Ask instead which services were emitting before and are not emitting now, and the answer arrives immediately:

11,806 → 0ledger-service span volume, before and during — the single query that names the culprit

Only then is the model called, and it is given the evidence rather than the database. It came back with:

ledger-service stopped emitting spans entirely (11,806 → 0); edge-gateway and payments /transfers calls into it fail and surface as root-span errors.

Confidence: high. Check ledger-service health now — restarts, OOMKills, readiness probes. Check for a rollout around 11:07Z. Verify its datastore: a dead database can kill the process before it emits a single span.

The root cause panel in Rocketgraph. A high-confidence badge, a headline stating that ledger-service has effectively stopped serving with a 91% span drop, an explanation of how that produces the alert, and a list of supporting facts each with a checkmark.
The panel opens itself when the alert is firing. The badge is the model's own confidence; every bullet beneath it is a number pulled from the telemetry.

Every claim there is traceable to one of the five panels. The model did not detect anything, and it could not have — it never saw the telemetry, only the summary. That constraint is deliberate: an explanation you cannot check is worth very little at three in the morning.

Why the ranking is not a model's opinion

There is a temptation to let the model decide what matters. We don't, for a boring reason: ask a model to rank the same findings twice and you will get two slightly different orderings.

So severity is computed — from the kind of change, the log level the engineer chose when they wrote the line, the words in the message, how far the number moved, and how much of the system's traffic it represents. It produces the same ordering every time, it costs nothing, and you can read the arithmetic.

The model's job is to explain an ordering, not to invent one. It also, usefully, disagrees sometimes: on a different alert it looked at three findings our scoring rated high and reasonably talked two of them down to low, because the trace showed a business decline rather than a fault. Both numbers are shown. Arithmetic ranks; judgement annotates.

Continuation of the root cause panel showing a 'not this' section ruling out the auth service, fraud service, the payments service itself and latency, each with the specific numbers that eliminate it, followed by an impact statement and a numbered list of next steps.
Saying what it is not is half the value. Each exclusion carries the number that rules it out, so the on-call engineer does not spend twenty minutes checking auth.

Show your working

The panel is only trusted if the evidence underneath it is one click away, so it is.

The expanded evidence panel showing four sections: services erroring with before and after counts including ledger-service down 92 percent, failing operations ranked by count, error log patterns grouped into templates with placeholders, and a link to one example failing request.
The same five queries, rendered raw. Services erroring with their pre-incident counts, the failing endpoints, and log lines collapsed into templates like `Exception on /accounts/<UUID>/statement` — one row instead of thousands.

Two details in that last panel are the whole design. The services table shows who is erroring more than before, which is how the blast radius gets bounded without reading a single log line. And the log lines have been collapsed into templates, so forty thousand unique messages with different account ids become one row that says Exception on /accounts/<UUID>/statement, with a count next to it and a note that it did not exist before.

What this is actually for

The bank exists so we can break it on purpose, at a scale where the answers are not obvious. Six services, sixty-three million spans a day, a failure that presents identically at four levels of the trace.

The result we care about is not the twenty seconds. It is that the sentence at the end of it — the ledger stopped emitting, here is the number, here is where to look — was assembled from five ordinary database queries and one request to a model that was never allowed to guess.

That is a system you can hand to whoever is on call tonight.

Frequently asked questions

How fast can distributed tracing detect a failed microservice?

In our reference bank, the first alert fired 20 seconds after we stopped the ledger service, and a second fired at 50 seconds. Both were rules whose healthy baseline is exactly zero — the count of ERROR-level logs, and the percentage of user-facing requests that fail. Rules that detect absence rather than errors are slower by design: a rule asking whether a service has gone quiet cannot fire until its evaluation window has drained the data from before the incident, which took 5 minutes 20 seconds.

Why do error-based alerts miss a crashed service?

Because a crashed service does not produce errors — it produces nothing. Its callers produce the errors, and they can only report that something downstream failed to answer. Every query that searches for errors, exceptions or failed spans will therefore describe the symptom accurately and never name the cause. The fix is to query for absence explicitly: compare each service's emission volume against the period before the incident and surface anything that collapsed.

What is the difference between an alert and a root cause?

An alert tells you a measurement crossed a threshold — error rate went above one percent. A root cause tells you which component broke and why that produces the measurement you are looking at. The gap between them is where on-call time is actually spent: identifying the service, pulling its logs, finding a failing request, and walking the call tree until the innermost failure appears.

Should an LLM decide which incidents are most severe?

No. Severity should be computed from facts — the kind of change, the log level the engineer chose when writing the line, the blast radius, the magnitude — because a model asked to rank fresh each time will produce slightly different orderings on identical input. Deterministic scoring means the same incident always ranks the same way. The model's job is to explain an ordering, not to invent one.

How much telemetry does a six-service application generate?

Our bank emits roughly 730 spans and 140 log records per second under steady load, which is about 63 million spans a day. Stored in ClickHouse this compresses about eleven to one — 660 GiB of raw telemetry occupies roughly 60 GiB on disk, with older partitions tiered automatically to object storage.

Kaushik Varanasi

Kaushik Varanasi

Founder & CEO, Rocketgraph

Kaushik founded Rocketgraph to make observability affordable at any scale. He writes about telemetry economics, object-storage architectures, and using AI agents to triage production incidents.

Read next