An endpoint that answers in milliseconds on its own and takes 3 seconds under load is not slow. It is serialized.
On a latency chart those two look the same, and they need opposite fixes.
Serialized means the work itself is fine, requests just take turns. In money systems the usual turnstile is one row: a balance, a running total, a per-user account that every request updates. A transaction grabs the lock, does its small bit of work, commits. The tenth concurrent request sits through nine turns before its own work even starts.
This hides well, because the tools you reach for all measure a request in isolation. EXPLAIN ANALYZE on that exact statement comes back instantly: it ran with nothing else competing for the row. The plan is optimal, the index is used, and no amount of query tuning will move the number. The cost does not sit in the plan. It sits in the queue in front of the plan.
The signal that splits the two cases is cheap: plot latency against concurrency instead of against payload size. Slow code degrades with input. Serialized code degrades with neighbours, so more concurrent callers push p95 up while the CPU stays bored.
The fix is structural rather than tactical. Stop mutating the aggregate on the write path: append immutable rows and derive the balance, or shard the hot row so contention spreads across many. A ledger that appends scales with hardware. A ledger that updates one row scales with nothing.
I put more of how I reason about contention in payment flows on my blog, here: https://polycratia.com/c/4c51a029
Worth knowing which row is the hottest in your system right now: a balance, a sequence, an idempotency table...