An endpoint's p99 is 4 seconds. The database's slow query log is empty and CPU is low. What is happening?
Show the full answer Hide the answer
What is being tested
Whether you know that the most expensive database problem in application code is invisible to database-side monitoring.
The mechanism
The code loads a collection, then loops over it accessing a related object. Each access issues a query. Each query takes 3 ms — well under any slow-query threshold, so none of them are logged. With 400 items, that is 400 round trips and 1.2 seconds of pure latency, plus connection acquisition and result parsing overhead.
The database is not slow. It is answering 400 questions quickly. The application asked 400 questions.
This is why the symptoms fit: low CPU (each query is trivial), empty slow log (each is fast), and a slow endpoint.
How to confirm it
- Count queries per request. Almost every framework and APM tool can report this. A request issuing more than a handful of queries per logical operation is suspicious; hundreds is diagnostic.
- Look at a distributed trace or a request-scoped query log. The pattern is unmistakable: the same query shape repeated with different parameters.
- Correlate query count with collection size. If a request over 10 items makes 12 queries and one over 400 items makes 402, you have your answer.
The fixes
- Eager loading — fetch the related objects in one query with the parent, or in a second batched
query using
IN. This is usually a one-line change and typically the entire fix. - A join, where the shape suits it.
- A batched loader that collects the IDs needed within a request and resolves them in one round trip. This is the standard answer where the access is spread across layers and eager loading is awkward.
The structural prevention
Bound the work per request. Pagination everywhere with hard maximums, a query-count budget enforced in tests, and a limit on collection size. Developer platforms operating at scale treat this as a design rule rather than an optimisation: the median object is fine and the outlier — a repository with 300,000 issues, an organisation with 50,000 members — is what falls over. A request that cannot be made fast should be paginated, made asynchronous, or refused, not attempted.
Why the other options are less likely
More memory helps when the working set does not fit, which would show as high I/O rather than low CPU. A lock would show as waiting sessions and would affect a specific operation, not p99 generally. Network saturation would degrade everything, not one endpoint — although note that the N+1 problem is fundamentally a round-trip problem, so the instinct about the network is not entirely wrong, just misplaced.