Turning on the observability stack is not free, and the ordering of what costs what is not the one most people expect. This page is the short version of the benchmark write-up, which has the method, the full ablations and the commands to reproduce everything here.
The first sections measure FastAPI on macOS. From FastAPI on Linux on, the numbers come from a 2-CPU Linux container on the same machine and cover all four frameworks.
Measured on a trivial async def endpoint returning {"ok": True}, uvicorn with a single worker,
loaded with ab -k -c 16 -n 20000:
--8<-- "benchmarks/README.md:headline"
Tuning is worth +80% RPS on the real server, and every setting behind it is a documented configuration field.
Each instrument alone, driving the ASGI app in-process so only library cost shows:
--8<-- "benchmarks/README.md:perinstrument"
Four things here are worth knowing before you tune anything:
- OpenTelemetry costs about twice what Sentry does, and is the dominant cost of the stack.
- Structlog costs nothing until you actually log.
LoggingInstrumentadds ~0.1 µs per request when configured. The cost arrives per record, not per request, and most of it is Sentry's two log handlers: on a three-record endpoint Sentry costs 12.3 µs per record, of which those handlers are 9.6. sentry_traces_sample_rate=1.0costs a further +353 µs per request. If you set it, set it next to a sample rate you actually want.- The Sentry knobs people reach for do nothing.
--8<-- "benchmarks/README.md:sentryablation"
attach_stacktrace, max_breadcrumbs and dropping the default integrations all measure inside
noise. Essentially the entire cost is the ASGI integration, and over half of that is a Transaction
built and discarded because sentry_traces_sample_rate is unset. That table configures
sentry-sdk directly. Since 2.0, lite-bootstrap leaves the framework integration out while tracing
is off and reports request errors itself, which costs about 6 µs per request on FastAPI
(FastAPI: Sentry).
In rough order of what they return:
| setting | saves | what you give up |
|---|---|---|
opentelemetry_sampler, 1% ratio |
~55 µs/req | 99% of your traces |
http_methods_to_capture=() on the Sentry ASGI integrations |
~35 µs/req | Sentry-side trace correlation |
opentelemetry_exclude_spans=["receive", "send"], the default since 2.0 |
~34 µs/req | the two ASGI event spans |
sentry_auto_session_tracking=False, the default since 2.0 |
~7.7 µs/req | Sentry release health |
sentry_logging_breadcrumb_level=None |
~7 µs per log record | log breadcrumbs on errors |
Together those are worth ~132 µs per request against the ~136 µs the tuned row actually recovers, so there is nothing else material hiding in it. That configuration is:
--8<-- "benchmarks/README.md:tuned"
One thing to leave off rather than turn on: the FastAPI access log
(logging_access_log_enabled) is off by default, and
turning it on makes every request emit a log record. On a service that otherwise logs nothing per
request, every request then pays the per-record cost measured here:
--8<-- "benchmarks/README.md:logging"
lite-bootstrap already applies eight savings for you. It turns Sentry Logs off in the logging
integration, because it never enables them and on 2.x the handler formats every record before
checking whether they are enabled; that one costs nothing. Since 2.0 it also leaves release-health
sessions off and starts breadcrumbs at WARNING, so an INFO record skips the breadcrumb handler.
Those two cost you release health and INFO breadcrumbs on error events; set
sentry_auto_session_tracking=True or sentry_logging_breadcrumb_level=logging.INFO to get them
back. And while Sentry tracing is off it reports request errors without sentry-sdk's framework
integration, which costs you request transactions and bodies the handler never read; set sentry_lean_error_capture=False to keep the integration. With tracing off it also turns
sentry-sdk's auto-enabling integrations off,
which costs you their SQL, Redis and outgoing-HTTP breadcrumbs; set
sentry_auto_enabling_integrations=True to keep them. On FastAPI, Litestar and FastMCP it also
records no span for the ASGI receive and send events, which costs you those spans; set
opentelemetry_exclude_spans=[] to get them back. With opentelemetry_metrics_endpoint set it
records HTTP metrics only through OpenTelemetry, which costs you the Prometheus HTTP metrics; set
prometheus_http_metrics_enabled=True to keep both. It also leaves health checks untraced, which
costs you probe traces; set opentelemetry_generate_health_check_spans=True to get them back. The
numbers on this page were measured before
2.0, against the old defaults.
Speed alone does not settle whether a setting is worth changing. Measured by capturing a real error
event with an incoming sentry-trace header and inspecting the envelope:
--8<-- "benchmarks/README.md:tradeoffs"
The row that needs a decision is http_methods_to_capture=(): the error event gets a fresh
trace_id and no parent_span_id, so errors stop correlating across services in Sentry. Take it
when OpenTelemetry owns distributed tracing and Sentry is only an error sink, which is the case in
any service running OpenTelemetryInstrument. Do not take it otherwise.
The same instruments one at a time, in-process, in the Linux container:
--8<-- "benchmarks/README.md:fastapilinux"
The ordering matches macOS: OpenTelemetry first, then Sentry, then Prometheus, with logging free until something logs. Since 2.0, Sentry with tracing off drops from about +59 to about +6 µs per request, below Prometheus (FastAPI: Sentry).
In-process, one setting at a time:
--8<-- "benchmarks/README.md:litestarinprocess"
The same app on Granian, over real sockets:
--8<-- "benchmarks/README.md:litestargranian"
Before 2.0, Sentry cost more on Litestar than on FastAPI, and all of it was LitestarIntegration.
Since 2.0, lite-bootstrap reports Litestar request errors itself while Sentry tracing is off, which
brings Sentry from about +71 to about +7 µs per request in-process
(Litestar: Sentry). The access log
(logging_access_log_enabled) is the largest opt-in item
left, at about 70 µs per request in-process.
Per consumed message:
--8<-- "benchmarks/README.md:faststreamconsume"
Per publish:
--8<-- "benchmarks/README.md:faststreampublish"
At the default faststream_log_level=WARNING the happy path logs nothing, which is why lite-bootstrap's
logging is cheaper than FastStream's own. Lowering it to INFO costs about 35 µs per message.
Per tool call, through the stateless JSON HTTP app:
--8<-- "benchmarks/README.md:fastmcp"
The baseline is large, so the instruments are a smaller share here than on the HTTP frameworks. OpenTelemetry is still the largest item. Since 2.0, Sentry with tracing off drops from about +82 to about +21 µs per tool call (FastMCP: Sentry).
--8<-- "benchmarks/README.md:otelfloor"
If you sample down to save CPU, this floor is what you keep paying. Excluding the receive and send
spans lowers it: Litestar's 1% row with them excluded sits below its 0% row with them kept. Since 2.0
they are excluded by default.
--8<-- "benchmarks/README.md:macos"
These ratios are an upper bound. The endpoint measured here does nothing, so the fixed per-request cost of the stack is compared against an unrealistically small denominator. A service that does real work per request, a database round trip or a downstream call, pays the same absolute cost against a much larger one.
Read the µs columns, not the percentages. A stack that costs 70% of a do-nothing handler's throughput costs a few percent of a handler that waits 5 ms on a database.
Numbers are machine-specific and move a few percent between runs. The ordering and ratios are stable across runs.