Case study: fraud scoring on every card authorisation, inside a 40 ms budget

How a payments provider moved its fraud model from one region to six, kept every decision under 40 ms and stopped failing open when the model was slow.

Contents
  1. Before
  2. What changed
  3. Results after 120 days
  4. What did not go smoothly
  5. In their words

Fintech

Payments provider, risk team

2,100 authorisations/s · 6 regions · card data residency in the EU and Brazil

Measured by the customer after at least 90 days in production. Index figures are relative costs.

p99 decision time
71 ms 34 ms
Decisions failed open
0.8% 0.02%
Regions serving traffic
1 6
Pages to on-call per month
14 2

Every card authorisation this provider processes goes through a fraud model before the bank answers. The card network gives them a hard budget: if the risk decision is not back in 40 ms, the authorisation continues without it. In the industry this is called failing open, and every failed-open decision is a transaction nobody scored.

Before

The model ran in a single US region on a managed inference service. European and Brazilian traffic crossed an ocean to reach it, so the median was fine and the tail was not: the p99 sat at 71 ms, and on a normal day 0.8% of decisions timed out and failed open. Fridays were worse.

Two things made this hard to fix in place. The card data of European and Brazilian issuers is not allowed to leave its jurisdiction, so “add a region” meant a second and third copy of the whole serving stack with its own deploy path. And the model is retrained weekly, so any multi-region setup had to roll a new version out everywhere without ever leaving a region on the old one for long.

What changed

The model, its feature-lookup service and the decision API moved into one Terrane deployment pinned to six regions, two of them residency-restricted:

services:
  - name: risk-features
    kind: http
    image: registry.internal/risk/features:2026.05
    latency_budget_ms: 8
  - name: risk-model
    kind: inference
    model: risk-xgb-ensemble-v41
    hardware: cpu
    latency_budget_ms: 20
  - name: decision-api
    kind: http
    image: registry.internal/risk/decision:2026.05
regions:
  serve: [iad, sfo, fra, ams, gru, sin]
  pin:
    fra: { data: eu-card }
    ams: { data: eu-card }
    gru: { data: br-card }
rollout:
  strategy: canary
  steps: [5, 25, 100]
  pause_on: { p99_ms: 36 }

Region pinning does the residency work: requests carrying EU card data can only be routed to fra or ams, and the runtime refuses a rollout that would leave those two regions without a healthy copy. The weekly retrain is a canary that pauses itself if the new version pushes p99 past 36 ms in any region, well inside the 40 ms line.

The model runs on CPU on purpose. At two thousand decisions a second the ensemble is cheap to evaluate, and a CPU tier in six regions costs less than accelerators in one.

Results after 120 days

Before After
p50 decision time 29 ms 18 ms
p99 decision time 71 ms 34 ms
Decisions failed open 0.8% 0.02%
Regions serving traffic 1 6
Weekly model rollout 4 hours, manual 35 minutes, automatic canary
Pages to on-call per month 14 2

The remaining failed-open decisions are almost entirely issuer-side timeouts that no serving change would recover.

What did not go smoothly

The feature-lookup service depended on a cache that was warm in the US and cold everywhere else. The first week in gru showed a p99 twice the budget while the cache filled. The team added a warm_pool: 1 node per region and a pre-warm job that replays the previous hour of traffic against a new region before it takes live requests. The runtime did not solve this; it made the problem visible per region so the fix was obvious.

In their words

“We spent two years explaining to the network why 0.8% of our decisions were blank. Now the conversation is about the model, which is the conversation we wanted.” — Head of risk engineering