A distributed-tracing interview answer gets much stronger when you can turn a flat list of spans into a causal story: which request waited, where it waited, and what you would verify before changing anything. You do not need a telemetry vendor to practice that skill. A compact Node.js script is enough to build a trace tree, flag broken parent links, and calculate where time is actually spent.
The useful distinction is simple: a trace follows one request; a span represents one timed operation inside it. The W3C Trace Context specification defines how a trace identity can travel across service boundaries, while OpenTelemetry’s trace documentation explains the parent-child model. In an interview, those definitions matter less than what you do with them.
What should you say before opening a dashboard?
Start with a hypothesis that can be disproved:
“I would isolate one slow trace, find its critical path, and separate self time from time spent in child spans. I would not call the database the bottleneck just because its span is long.”
That last sentence avoids a common mistake. A 120 ms API span is often just a container around 110 ms of downstream work. Its inclusive duration is 120 ms, but its self time is only 10 ms.
The small exercise below does four things:
- turns spans into a tree;
- identifies roots and orphaned spans;
- computes each span’s child time and self time;
- prints a ranked, interview-friendly summary.
Build a tiny trace-tree analyzer
Save this as trace-drill.mjs and run node trace-drill.mjs. It uses no packages.
import assert from "node:assert/strict";
const spans = [
{ id: "a", parentId: null, service: "web", name: "GET /checkout", start: 0, end: 142 },
{ id: "b", parentId: "a", service: "api", name: "validate cart", start: 8, end: 136 },
{ id: "c", parentId: "b", service: "pricing", name: "quote", start: 18, end: 48 },
{ id: "d", parentId: "b", service: "db", name: "load inventory", start: 52, end: 128 },
{ id: "e", parentId: "missing", service: "email", name: "send receipt", start: 140, end: 160 },
];
function duration(span) {
if (!Number.isFinite(span.start) || !Number.isFinite(span.end) || span.end < span.start) {
throw new Error(`Invalid timing for span ${span.id}`);
}
return span.end - span.start;
}
function analyze(input) {
const byId = new Map(input.map((span) => [span.id, { ...span, children: [] }]));
if (byId.size !== input.length) throw new Error("Span ids must be unique");
const roots = [];
const orphans = [];
for (const span of byId.values()) {
if (!span.parentId) {
roots.push(span);
continue;
}
const parent = byId.get(span.parentId);
if (!parent) {
orphans.push(span);
continue;
}
parent.children.push(span);
}
const rows = [...byId.values()].map((span) => {
const total = duration(span);
const childTime = span.children.reduce((sum, child) => sum + duration(child), 0);
return {
id: span.id,
service: span.service,
operation: span.name,
total,
childTime,
selfTime: total - childTime,
};
});
return { roots, orphans, rows: rows.sort((x, y) => y.selfTime - x.selfTime) };
}
const result = analyze(spans);
console.table(result.rows);
console.log("roots:", result.roots.map((span) => span.id));
console.log("orphans:", result.orphans.map((span) => span.id));
assert.deepEqual(result.roots.map((span) => span.id), ["a"]);
assert.deepEqual(result.orphans.map((span) => span.id), ["e"]);
assert.equal(result.rows[0].service, "db");
assert.equal(result.rows[0].selfTime, 76);
assert.equal(result.rows.find((row) => row.id === "b").selfTime, 22);
For the sample data, db/load inventory has 76 ms of self time. The API span lasts 128 ms, but 106 ms of that is in its two children. That is a defensible observation; “the API is slow” is not.
Why this is more useful than sorting spans by duration
A flat sort is tempting, but it loses the relationship that makes traces valuable.
| Observation | Weak conclusion | Better next question |
|---|---|---|
| API span is 128 ms | “The API is slow.” | How much is its self time versus downstream time? |
| DB span is 76 ms | “Add an index.” | Which query, lock, connection wait, or replica route produced it? |
| Email span has no parent | “Ignore it.” | Was context propagation missing, sampled out, or did this start asynchronously? |
The analyzer is intentionally small, so it also exposes its limits. It sums direct child durations and assumes they do not overlap. Real traces can contain parallel children, retries, clock skew, and asynchronous handoffs. Say that out loud in an interview. It shows you understand that observability data is evidence, not a verdict.
Turn the output into a 60-second interview answer
Here is a compact structure that works for most “how would you debug latency?” prompts:
- Scope: “I’d start with one representative slow trace and confirm the user-facing symptom.”
- Decompose: “I’d compare inclusive duration with self time to find the critical path rather than blame the outer span.”
- Verify: “For the inventory call, I’d correlate trace IDs with query timing, connection-pool wait, lock metrics, and error/retry counts.”
- Change safely: “If the evidence points to a query, I’d reproduce with the same parameters, inspect the plan, and make a reversible change behind a measurable guardrail.”
- Close the loop: “I’d track p50, p95, and error rate after the change, then keep an alert on the dependency’s self time.”
Notice that this answer never promises a magical fix. It explains a sequence of evidence, decisions, and rollback criteria. That is what makes it credible.
How can you make the drill harder?
Use the same script with three variations:
- Add two child spans that overlap; update the calculation so unioned child intervals, not their raw sum, are subtracted.
- Add a retry span and explain why a successful final response can still have poor tail latency.
- Add a missing parent and decide whether it is an instrumentation bug or an intentionally detached background task.
Each variation gives an interviewer a natural follow-up. More importantly, it gives you a way to practice reasoning out loud instead of memorizing “use tracing” as a slogan.
If you want a partner to push those follow-ups and review how you explain a technical exercise, aceround.app — an AI interview assistant can help you review a mock-interview transcript. The script should remain your source of truth; the practice value comes from defending the investigation sequence.
FAQ
Is the longest span always the bottleneck?
No. A parent span may be long because it includes child work. Compare self time, dependency timing, and the request’s critical path before deciding where to investigate.
What metrics should accompany traces?
For a dependency, start with request rate, error rate, p50/p95/p99 latency, saturation or queue depth, and a relevant domain metric such as cache-hit rate. A trace tells you where one request went; metrics tell you whether the pattern is widespread.
What should I do with orphaned spans?
Treat them as a signal. Check propagation headers, sampler behavior, asynchronous boundaries, and ingestion delays before discarding them. An orphan can be an instrumentation gap—or a real background job that needs a different relationship model.
AI disclosure: This post was drafted with AI assistance and manually reviewed for technical accuracy and originality. The code and examples are original to this post.
Top comments (0)