A fast database rules out one suspect. It tells you nothing about the request.
Query time gets measured inside the engine. Request time is everything around it: waiting for a connection, the number of round trips, serialization, and the part that usually dominates, calls to systems you do not own.
Three mechanics, in the order I usually find them.
Round trips, not query cost. Forty queries at two milliseconds each is a fast database and a slow endpoint. Concurrency makes it worse: each of those forty trips holds a pooled connection, so the pool starts rejecting work long before the engine breaks a sweat.
Inherited tail latency. Shipping rates from several carriers, a verification provider's decision, a quote from an exchange: once any of that sits on the request path, your p99 is the worst p99 in the chain. It is not your code, and no amount of indexing touches it.
Missing timeouts. A lot of HTTP clients default to no timeout at all. Your averages stay clean because the slow path is rare, right up until a provider degrades and requests pile up behind calls that are never coming back.
How I handle it: every endpoint gets an explicit latency budget, and every outbound call declares its share of that budget with a timeout and a defined fallback. Anything not required to produce the response goes to a queue: notifications, label generation, indexing, analytics. And I read spans instead of averages, because averages hide exactly the requests people complain about.
The database is rarely the story. Most of the time I find it sitting on the request path.
I write up more of these failure modes on my blog, if that is useful: https://polycratia.com/c/6d6a50c1
Next time you trace a slow endpoint end to end, I think the answer will surprise you: round trips, a third party, or just waiting on a connection...