Shipping ItLesson 28 of 74

Contents

Shipping It

Lesson 28 of 74. Sign in to keep your place.

Module 3 · Knowing what it is doing

Finding the slow bit

10 min read

From a request ID to a shape

You already give every request an ID and log it everywhere. A trace is the next step: instead of scattered lines, you get a tree of spans, each with a start time and a duration, all sharing one trace ID.

Text
GET /orders                                     840ms
├─ auth.verify                                    4ms
├─ db.query orders                               31ms
├─ loop over 40 orders
│  ├─ db.query customer                           3ms
│  ├─ db.query customer                           3ms
│  └─ ... 38 more                               114ms
└─ http POST payments.example.com                610ms

You do not have to guess. The picture says the request spent 610ms waiting for a third party and 120ms doing a query it ran forty times.

The three culprits, in order of frequency

The N+1 query. One query to fetch a list, then one query per item. Forty items at 3ms is 120ms of latency created by a loop that reads perfectly well in code review. It is invisible until you count queries per request, and it scales with your data, so it appears three months after the code is written.

A third-party call with no timeout. Their p99 becomes your p99. Every outbound HTTP call needs an explicit timeout, and here is the subtlety: a 30-second timeout on a call inside a request that itself times out at 10 seconds is not a timeout. Set it below your own budget. Then decide what to do when it fires: a degraded page is usually better than an error page.

Waiting for a resource. The request spends 800ms not working at all: waiting for a connection from the pool, waiting for a lock, waiting behind a queue. This is the one that looks like “the database is slow” when the database is idle. If you instrument only the query and not the wait to acquire a connection, this time is invisible and you will chase the wrong thing for a day.

You can do this without a tracing backend

Tracing tools are good and you do not need one to start. Log a duration at each boundary with the request ID:

Text
req=a7f3 span=db.orders ms=31
req=a7f3 span=http.payments ms=610
req=a7f3 span=total ms=840

Sort by ms. That is 80% of a trace for an afternoon’s work, and it works with the log tooling you already have.

The database’s own answers

Two Postgres features do most of the work:

EXPLAIN (ANALYZE, BUFFERS) runs the query and shows you the plan with real timings and real row counts. The thing to look for is a large gap between estimated and actual rows: that means the planner chose a strategy based on a wrong guess, which is how a query that was fast last month is slow now.

pg_stat_statements ranks queries by total time across all executions. This is where the counter-intuitive finding lives: the query taking 3ms but running 40,000 times an hour costs far more than the 900ms report somebody complained about. Optimising by total time rather than by worst single time is usually the higher-value order.

Sampling, when there is too much

Tracing every request at high traffic is expensive to store. The standard answer is tail sampling: keep 100% of traces that errored or exceeded a latency threshold, and 1% of the rest. You keep every interesting trace and pay for almost none of the boring ones.

The free path

OpenTelemetry is the vendor-neutral standard for producing traces; instrument once and you can change backend later. For the backend: Jaeger and Grafana Tempo are free and self-hostable; SigNoz bundles traces, metrics and logs in one open-source install. Honeycomb, Datadog and New Relic are the paid options, and Honeycomb in particular is very good at the high-cardinality questions (“which requests are slow, and what do they have in common?”) that metrics cannot answer.

Start with timed log lines. Move to real tracing when you can no longer answer “where did the time go” in one pass.

Every lesson here is open. Your progress is recorded once you pass the Agents and Automation exam.

Check your understanding

A report endpoint takes 4 seconds. The database is at 12% CPU, and each of the queries the endpoint runs takes under 10ms when you run it by hand. Which explanation fits best?

Pick the one you would defend. Your answer stays private.

Next lesson · 9 minThe bill, and the traffic that runs it up

Stuck on something?Ask Tri, your AI tutor

How to cite: Addaly (2026) ‘Finding the slow bit’, in Shipping It. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/cloud-and-shipping/tracing-one-slow-request (Accessed: 13 October 2026).