Getting slower with every win.
The platform worked, and that was the problem. Every new merchant added load, and p95 on the payment-status endpoint had crept past a second. Checkout felt sticky. The team's instinct — a reasonable one — was to scale the cluster up and out.
We asked them to hold off on the credit-card-for-AWS solution for a week and let us measure first. Throwing hardware at a latency problem usually just makes the same bad pattern more expensive.
The latency was in the round-trips, not the servers.
We added distributed tracing and watched a week of real traffic before changing a line. The hot path was making more network calls than anyone realised.
- 01A synchronous fraud-scoring call sat in the request path, adding 300–500 ms to every payment.Critical
- 02Issuer metadata was fetched from a third-party API on every request, uncached.High
- 03Settlement lookups ran N+1 — one query per line item, no batching.High
- 04Three internal services were chained sequentially where one aggregated call would do.High
- 05A composite index was missing on the busiest filter, forcing a sequential scan.Medium
Measure, fix the hot path, leave the rest alone.
We didn't touch anything that wasn't on the critical path. The goal was the smallest set of changes that moved p95, each one measured under production-like load before and after. No rewrite, no new datastore, no migration weekend.
Everything shipped behind a flag with a one-command rollback, and we load-tested each change against a replayed copy of real traffic so the numbers were honest.
Five changes, in the order they shipped.
Made fraud scoring asynchronous.
Replaced the blocking call with a fast local pre-check and moved full scoring off the request path. Median dropped 380 ms overnight.
Cached issuer metadata in Redis.
The third-party lookup went from every-request to a warm cache with a short TTL and background refresh. One fewer external dependency on the hot path.
Batched the settlement lookups.
Collapsed the N+1 into a single batched query and added the missing composite index. The endpoint stopped scanning.
Collapsed three service calls into one.
Introduced an aggregation endpoint so the gateway made a single internal call instead of a sequential chain of three.
Gave the team the traces to keep it fast.
Left tracing, latency SLOs, and a k6 load profile wired into CI so a regression shows up in a PR, not in production.
A platform that gets faster as it grows up.
Demo-grade request path
- · synchronous fraud check
- · uncached issuer lookups
- · N+1 settlement queries
- · sequential service chain
- · no latency budget
Production-grade hot path
- · async scoring + fast pre-check
- · warm Redis cache
- · batched + indexed queries
- · single aggregated call
- · SLOs enforced in CI
p95 settled at 350 ms and has held there through a year of growth, with uptime at 99.98%. No new servers were added. The team now owns the traces and the load profile, so the next regression is a failed check rather than an incident.