field note / debugging · operations · distributed-systems

Debugging distributed data from the outside in

Why I begin with the raw request and persisted state, then move inward toward logs and code.

· 6 min

A distributed-system incident invites premature storytelling. A dashboard shows a gap, a replica looks suspicious, a queue has depth, and soon there is a neat explanation assembled from indirect evidence.

Neat explanations are dangerous when the system can answer directly.

My preferred debugging loop is deliberately plain:

raw request → response → persisted state → cache/queue → targeted logs → code path

I move from the system's external contract toward its implementation. This keeps hypotheses cheap and disposable.

Reproduce the contract

Start with the smallest request that demonstrates the failure. Save the exact URL, method, tenant or account context, important headers, and a redacted response.

curl is useful because it removes frontend state and client abstractions:

curl --fail-with-body \
  -H "authorization: Bearer $TOKEN" \
  -H "accept: application/json" \
  "https://service.example/v1/routes?asset_id=$ASSET"

The goal is not to prove the API is broken. The goal is to establish one stable observation: for this identity and input, at this time, the service returned this status and body.

If the bug cannot be reproduced, compare context before reading code: environment, organization, permissions, time range, pagination cursor, and cache state. Many “same request” claims are not the same request.

Ask each store directly

When an API response is missing data, I inspect the source-of-truth row before assuming a rendering or serialization bug. Then I inspect the state that gates whether that row is visible.

For a route-like workflow, that might mean checking:

  • does the route exist in PostgreSQL?
  • does its tenant match the caller?
  • is there a start event, and when was it created?
  • is the reading timestamp before or after that boundary?
  • does Redis contain the expected current-state hash?
  • was the key written under the same namespace the reader uses?

A useful investigation table looks like this:

Boundary Expected Observed
API route present absent
PostgreSQL route row present
event state started present at 14:03
reading time after start 14:02
Redis active route key present

At that point the problem is no longer “the endpoint sometimes misses routes.” It is a timestamp-boundary decision. The search space collapsed without reading a hundred log lines.

Logs answer a question

Cloud logs are most useful after the question is narrow. Instead of searching for an asset id across an hour of output, search the function or decision that could explain the observed boundary.

Good diagnostic messages contain the values needed to reconstruct that decision:

Logger.info(
  "route_gate asset=#{asset_id} reading_at=#{reading_at} " <>
    "started_at=#{started_at} result=#{result}"
)

Do not rely only on structured metadata if the production log viewer truncates or hides it. The critical count, timestamp, and result should survive in the message itself.

Also log decisions, not narration. “Processing route” says that a function ran. “route_gate … result=before_start” says why behavior occurred.

Keep competing hypotheses alive

A strong hypothesis predicts evidence. “The read replica is stale” predicts that primary and replica disagree for a bounded period. “A race creates the event after the reading” predicts an ordering visible in timestamps or transaction history. “The cache is poisoned by rejected data” predicts a mismatch between accepted rows and cached current state.

Write these predictions before collecting more evidence. Then mark them:

Hypothesis Evidence State
stale replica primary and replica contain same row
timestamp gate reading precedes start event
missing cache key exists with correct tenant

This is not ceremony. It prevents a favorite theory from absorbing every new fact.

Trace inward only as far as needed

Once the failed boundary is known, trace the code path that implements it. At this stage, source exploration is precise: endpoint, context function, query, cache lookup, event predicate. The code explains the observation instead of generating unbounded possibilities.

The eventual regression test should reproduce the boundary, not the production anecdote. If a reading before a mission-start event must be excluded, create those two timestamps explicitly and assert both sides. If late data should be stored but should not replace current state, assert both persistence and cache behavior.

Finish with operational proof

A passing test proves the chosen model. Deployment verification proves the running system matches it.

After the change:

  1. exercise the raw request again;
  2. inspect the relevant row and cache key;
  3. confirm the new decision log appears with concrete values;
  4. verify a negative case as well as the happy path;
  5. keep a short record of command, response, and timestamp.

Distributed debugging is often described as an observability problem. It is also an epistemology problem: what do we actually know, which component told us, and what evidence would make us change our mind?

The shortest path is usually not more telemetry. It is asking the existing system one sharp question at a time.