Three seconds in an API is rarely three seconds of work. Usually it's one request waiting on a lock that another request holds across a network call.
The shape shows up all over payment code: open a transaction, take an exclusive lock on the balance row, call the provider, write the result, commit. Correct, readable, and it holds that row for the whole provider round trip.
At one request per second nobody notices. Put twenty requests on the same wallet or the same merchant account and latency goes serial: each one waits for every provider call ahead of it. The database is idle. CPU is idle. The slow query log is empty, because nothing was slow. Things were queued.
The tell: latency tracks concurrency on a single key, not data volume. The endpoint is fine for most of your users and awful for your busiest one, with identical SQL.
How I handle it now: no I/O I don't control inside a transaction. I record the intent and commit. The provider gets called outside any lock. Then a short transaction, keyed by an idempotency key, to apply the outcome. Where the ledger allows it I append entries instead of mutating one row, so writers stop competing for that lock at all.
Lock hold time is a latency budget you spend on behalf of every concurrent caller. I keep two explicit lists, operations that may hold a lock and operations that may touch the network, and I don't let them overlap.
I wrote up more of these failure modes on the blog: https://polycratia.com/c/a97c2017
Worth knowing where your hottest row lives: a user balance, an order, or a counter...