A fast database and a slow API usually point at the same thing: most of the request never happened in the database at all.
Query execution scales with rows scanned. Response assembly scales with objects returned. Two different budgets, and only one of them shows up on your database dashboard.
Once rows leave the driver you keep paying per item: ORM object construction, Decimal money formatting, timestamp localisation, nested serializers walking relations, and authorisation decisions your application makes in a loop instead of in a WHERE clause. Zero extra queries, and still seconds of work. None of it lands in a slow query log, because none of it is a query.
That is how an endpoint that felt fine at launch gets slow later with no query regressing anywhere. Nothing degraded: accounts just got older. A transaction or ledger endpoint with no upper bound on its result set has latency proportional to customer tenure, and the oldest accounts are usually the ones you care about most.
So I bound the response in the contract before I optimise anything inside it. Hard page limits, explicit field sets, no "return everything" default. Aggregates get computed where the data already lives, so a balance comes back as one number instead of thousands of rows summed in application code. Per-item permission checks turn into a predicate rather than an iteration.
My take: "fast database, slow API" almost always means the work is proportional to the response, and the response has no ceiling. Fixing the query is the wrong instinct. What helps is refusing to produce unbounded output in the first place.
I write up more of these request-path post-mortems on my blog, if that's useful: https://polycratia.com/c/70123284
If you run payment or ledger APIs: did your slowest endpoint turn out slow per query, or slow per row returned?