A compliance moderator told me every action in her review queue took about three seconds. The rest of the team said the app felt fine.
The handler timings agreed with them. The view ran its query, serialized the result and returned it, fast and consistent. So I stopped trusting the view and measured where the request entered the process instead.
The three seconds were gone before the handler ran.
Auth middleware introspected the token against the OAuth service on every request, and that service expanded roles into an effective permission set by walking the role hierarchy on each call. Latency scaled with how many roles the user carried, not with the data requested. Regular users had two roles and did not notice. Moderators carried everything, and paid for it on every click.
That is why it stayed invisible: the slow tail belonged to a handful of internal users, the aggregate buried them, and a handler-level timer does not cover code that runs before the handler.
Two changes. The expanded permission set is cached per token now, with a short TTL and explicit invalidation on role change, because permission checks are read-heavy and change rarely. And request timing moved to the edge, so middleware time lands in the same number you already watch.
What I do differently now: when a latency complaint comes from one user and not the aggregate, I look at what is different about that user's identity before I look at the query plan. Permissions are a hot path, and few people treat them like one.
I write these up in longer form on my blog, if that is useful: https://polycratia.com/c/91b83cc2
So it is worth knowing where your request timer actually starts: at the edge, or at the handler.