Have you ever set a single latency target for a FHIR API and watched half your endpoints breeze past it while a few sit at the cliff every day? A global latency budget looks tidy on a slide, and it lies to the team that is supposed to defend it. The traffic mix on a modern FHIR deployment is too varied for one number to describe.
The habit that actually holds up is a per-resource budget. Each resource type carries its own workload shape, its own cache profile, and its own tolerance for tail latency. For the wider set of walkthroughs, the FHIR fundamentals hub collects the surrounding material.
Not Every Endpoint Deserves the Same Number
Reading a Patient resource is fast because the payload is small and the cache hit rate is high. Reading a Bundle of a hundred Observations is slow because the payload grows with the entries. Reading a search across MedicationRequests with three parameters and a _include is slower still.
Setting a single global budget of a hundred milliseconds means Patient reads will look pointlessly generous while Bundle reads look chronically over. A pass through the site's FHIR p99 latency lookup shows the published tail figure per resource, per engine, and per call category, which is exactly the granularity the budget needs.
The p50 Number Hides the Story
Here is the thing: p50 says nothing about the experience of the users who ended up in the tail. A latency budget expressed as p50 targets the median user and lets the ninety-ninth percentile drift wherever the traffic mix takes it.
Per-resource budgets should live at p95 and p99. When the two numbers are named together, the budget captures both the typical case and the tail. For the specific gap between them, the p50 vs p99 gap that kills FHIR user experience walks through the cost.
Resource Types Behave Differently at Scale
Some resources scale predictably. Patient reads stay under a few milliseconds no matter how much you push them. Others show a knee in the curve where latency spikes past a certain load. Observation searches with wide date ranges are the most common example.
Naming the knee per resource type gives operations a signal to alert on before latency drops become user-visible. A budget that only tracks total RPS misses the knee entirely.
Caching Interacts With the Budget in Odd Ways
Caches can make latency budgets look better than they are. During a warm-cache window every read is fast, and the budget stays green. When the cache clears, the same reads hit the backend and slip past the budget for as long as the cache takes to warm again.
Per-resource budgets should be measured on both cold-cache and warm-cache traffic separately, or at minimum with a cache-hit-ratio column next to the latency number. For the operational surface of that trap, when caching hides FHIR latency until the cache expires walks through the mechanics.
Writing the Per-Resource Budget
The budget document should list every resource type the deployment handles, along with:
- The p95 latency target for reads.
- The p99 latency target for reads.
- The p95 and p99 targets for the batch endpoint if applicable.
- The cache hit ratio the budget assumes.
- The knee-of-curve RPS beyond which alerts fire.
Six numbers per resource type is not overhead; it is the observability surface that lets operations know what to watch. Every alert should trace to one of these numbers.
Bundle Size Deserves Its Own Column
Bundle traffic is where global budgets fall apart fastest. A Bundle of ten entries and a Bundle of two hundred entries share nothing operationally. The budget should express the p99 tail as a function of Bundle size, not a single number. For the mechanics of that curve, how Bundle size drives tail latency in FHIR covers the shape.
The truth is that global latency budgets are a comforting fiction. Per-resource budgets read more work to write, and they hold up under real traffic. That is the trade every deployment ends up making eventually.

Sources
- HL7 FHIR core specification of HTTP interactions covering - HL7 FHIR core specification of HTTP interactions covering read, write, and batch semantics behind per-resource latency budgets