WritingDatabricks (DBRX)Databricks (DBRX)published Jul 16, 2026seen 1w

What happens in the milliseconds after you tap pay

Open original ↗

Captured source

source ↗

What happens in the milliseconds after you tap pay | Databricks Blog Skip to main content

Summary

A sample Databricks App (FastAPI + React) that scores credit card transactions for fraud in real time using Model Serving route optimization for low-latency inference and Lakebase Postgres for online feature and profile lookups.

Fast inference alone is not enough. The app pairs route-optimized Model Serving with Lakebase, plus the connection pooling, OAuth token rotation, and autoscaling patterns that keep latency stable under load.

Across 5,000 requests, the route-optimized endpoint returned in 27 ms at p50 and 37 ms at p95 end-to-end, with 8.9 ms median Lakebase feature lookups and 100% success, well inside checkout latency budgets.

You're standing at the register. You tap your card. A tiny spinner appears for maybe half a second, maybe less, and then it says Approved. Or it doesn't. During that time, something had to decide whether this charge looks like you, or like someone who stole your card number in a data breach six months ago. It had to know things about you: your spending patterns, your daily limit, whether you even allow purchases from other countries. And it had to do all of that fast enough that you didn't notice it happened. This post is about what that "something" looks like when you build it on Databricks. We'll walk through retail-app, a sample application (FastAPI backend, React frontend, deployed as a Databricks App) that brings two platform capabilities together: Model Serving with route optimization, a faster network path to your deployed model. Lakebase, a managed Postgres for the profile and feature data the model needs at prediction time, featuring autoscaling so the database scales with demand instead of becoming the new bottleneck.

The full repo is on GitHub , you can fork it, deploy it to your workspace, and tap "pay" yourself. The flow: what actually happens on a transaction Before we look at code, here's the plain story of a single payment. Two checks run in sequence: a model scores the charge, then the app checks your profile rules. Either one can decline the transaction. The model runs first because we want its latency numbers regardless of the outcome. Then the profile lookup (daily spending cap, international transaction toggle, country of residence) feeds a handful of if-statements. The response includes timing for every step so you can see exactly where the milliseconds went. Updating your profile (changing your daily limit, toggling international transactions) is a separate action. You save changes to the database, and the next payment picks them up. No redeployment, no cache invalidation. Route optimization: why the network path matters When a model is deployed behind Databricks Model Serving, there's a network hop between your application and the inference container. For batch workloads, a few extra milliseconds per request is irrelevant. For a checkout experience, it's everything. Route optimization shortens that network path. When you enable route optimization on an endpoint, Databricks Model Serving improves the network path for inference requests, resulting in faster, more direct communication between your client and the model. This optimized routing unlocks higher queries per second (QPS) compared to non-optimized endpoints and provides more stable and lower latencies for your applications. You enable it when you create the endpoint, and you query through the data-plane flow using OAuth, not personal access tokens. You get lower latency and higher throughput for the same compute, which is exactly what an interactive fraud-scoring use case needs. In the sample app, the endpoint is called fraud-detection-lakebase . Here's the constant and the function that calls it:

A few things to notice: serving_endpoints_data_plane.query : This is the data-plane query path, which is what route optimization uses. The Databricks SDK handles the OAuth token exchange under the hood. asyncio.to_thread : The SDK's query method is synchronous. Wrapping it in to_thread keeps the FastAPI event loop free while the model runs. dataframe_records : The payload is a list of dictionaries (one per row). For fraud scoring, we send one transaction at a time.

The model itself returns fraud_probability , fraud_flag , and (crucially) its own internal timing: lookup_ms (how long the feature lookup inside the model container took), inference_ms (CatBoost prediction), and total_ms . The backend maps these through so the frontend can show a latency waterfall:

So when you see the latency breakdown in the UI ("Model Inference: 45ms" with "Feature Lookup: 8ms" nested underneath), they're the numbers measured at each layer and stitched together in a single response. For more on setting this up: Route optimization · Querying route-optimized endpoints . Lakebase: Postgres for the data the model needs The fraud model doesn't just look at the transaction in isolation. It looks up the customer's historical features (average transaction amount, cross-border ratio, chargeback rate, velocity over the past 24 hours) using the first six digits of the credit card (the BIN) as the lookup key. Those features live in a Lakebase Postgres table called customer_features . This is the same table the backend reads from for profile data (name, daily limit, international toggle). One table, two readers: the model container reads features for inference, the FastAPI app reads profile fields for business rules. On the read side, the backend borrows a connection from the pool, runs a parameterized SELECT by user_id , and returns the connection in a finally block. Straightforward psycopg2, but the borrow/return discipline matters when this runs on every transaction. The query uses parameterized placeholders for user input, so the lookup key is never concatenated into the SQL. On the write side, when a user changes their daily limit or toggles international transactions in the UI, the backend builds a dynamic UPDATE from an allowlist of editable columns. Only fields on that list are written; the client can't inject arbitrary column names. The write runs with autocommit = True , so the change is immediately visible: the very next transaction sees the updated limit without waiting for a batch flush or cache invalidation. Connection pooling and OAuth token rotation Every transaction in this app hits Lakebase at least twice:...

Excerpt shown — open the source for the full document.