The database graphs were boring: fast queries, nothing in the slow query log, no lock waits. And the moderation panel the compliance team uses was still crawling, sometimes it just hung.
So I went looking somewhere else and found the app was out of connections. Every worker held one. Not to run a query: to wait.
The code did something that reads perfectly well: open a transaction, write the verification record, call the KYC provider over HTTP, write the result, commit. One atomic unit, no half-finished checks. I had written that shape myself more than once.
What it actually does is pin a database connection for the full duration of someone else's network latency. The provider got slow and my pool drained. Every unrelated request queued behind it, including the cheap reads that needed the database for a millisecond. The database sat idle while the API was down.
What I do differently now: transaction boundaries don't contain a network call. Write intent, commit, call out, then open a second short transaction to record the outcome, and make the in-between state an explicit step in the state machine instead of an accident. I graph pool wait time next to query time, because they fail in opposite directions.
An API's latency is a sum of waits, and the database dashboard reports exactly one of them. The expensive ones are usually for resources the database owns but your code is holding.
I write up more of these production post-mortems on my blog: https://polycratia.com/c/64efe93c
Your pool wait shows up somewhere today: in a metric, or only in user complaints...