The N+1 you cannot see
An endpoint returning fifty posts with their authors was taking 1.4 seconds. Nothing in the trace stood out. No single query was slow; the slowest was four milliseconds. The database dashboard was calm.
That is the shape of an N+1. One query fetches the posts, then the ORM quietly issues one more per post to load its author. Fifty-one round trips, each individually unremarkable, adding up to most of the response.
Why profilers miss it
Most profiling views sort by the cost of a single call. An N+1 has no expensive single call — it has a cheap one repeated. Unless your tooling groups by query shape and shows you the count, the pattern is invisible. The fix to your tooling is usually more valuable than the fix to the endpoint: log query counts per request, and alert when one request issues more than a threshold.
The fix, and the fix that lasts
The immediate fix is to collect the author ids and fetch them in one statement, or to join in the original query. That took twenty minutes.
The lasting fix was adding an assertion to our integration tests: this endpoint must complete in fewer than five queries. It has caught the same regression twice since, both times introduced by a well-meaning change to a serializer that added a field nobody thought was expensive.
Caching would also have made the endpoint fast. It would not have made it correct, and it would have moved the cost to the first request after every deploy.