Skip to content

Performance

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 short answer

Measured on a trivial async def endpoint returning {"ok": True}, uvicorn with a single worker, loaded with ab -k -c 16 -n 20000:

Real server, uvicorn + ab -k, trivial async endpoint:

config RPS µs/req vs bare
bare FastAPI 7698 129.9 -
full lite-bootstrap stack (otel + prometheus + structlog + sentry) 2313 432.3 −70%
same stack, tuned (§6) 4164 240.2 −46%

Same endpoint plus three structlog records per request:

config RPS µs/req vs bare
bare 5548 180.3 -
full stack 1944 514.5 −65%
tuned 3418 292.5 −38%

Tuning is worth +80% RPS on the real server, and every setting behind it is a documented configuration field.

Where the time goes

Each instrument alone, driving the ASGI app in-process so only library cost shows:

instrument RPS +µs/req share of full stack
LoggingInstrument (configured, no logs emitted) 61133 +0.1 ~0%
PrometheusInstrument 29470 +17.7 8%
SentryInstrument (tracing off) 13541 +57.6 25%
OpenTelemetryInstrument 7280 +121.1 53%
all four 4056 +230.3

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. LoggingInstrument adds ~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.0 costs 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.
scenario +µs reading
defaults (tracing off) +64.2 the number to beat
attach_stacktrace=False +63.2 no effect on the happy path
max_breadcrumbs=0 +63.4 no effect - the crumb is still built
disabled_integrations=[Stdlib, Modules, Dedupe, Excepthook, Threading] +63.8 no effect
default_integrations=False (Starlette+FastAPI kept) +63.8 no effect
integrations=[], no framework integration −0.1 all of it is the ASGI integration
auto_session_tracking=False +56.5 sessions cost ~7.7 µs
http_methods_to_capture=() (no Transaction) +29.2 the Transaction costs ~35 µs
both of the above +19.9

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.

If you are RPS-constrained, start here

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, FastAPI and Litestar ~34 µs/req the two ASGI event spans
sentry_auto_session_tracking=False ~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:

FastAPIConfig(
    # Sentry: OTel owns distributed tracing, Sentry is an error sink
    sentry_integrations=[
        StarletteIntegration(http_methods_to_capture=()),
        FastApiIntegration(http_methods_to_capture=()),
    ],
    sentry_logging_breadcrumb_level=None,
    sentry_auto_session_tracking=False,
    # OpenTelemetry: sampling is the single biggest saving here
    opentelemetry_exclude_spans=["receive", "send"],
    opentelemetry_sampler=ParentBased(TraceIdRatioBased(0.01)),
)

One thing to leave off rather than turn on: the FastAPI access log (fastapi_logging_middleware_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:

Three records per request, in-process:

scenario +µs/req delta
sentry-sdk defaults +101.1
sentry_logs_level=None (lite-bootstrap's default since #186) +93.8 −2.4 µs/record
also sentry_logging_breadcrumb_level=None +72.4 −9.6 µs/record total

lite-bootstrap already applies one saving for you: it passes sentry_logs_level=None by default, because it never enables Sentry Logs and the handler formats every record before checking whether they are enabled. That one costs nothing, which is why it is a default rather than a knob.

What each saving costs you

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:

config txn name continues incoming trace breadcrumbs
defaults /ping yes yes
http_methods_to_capture=() /ping no yes
sentry_logging_breadcrumb_level=None /ping yes no
propagation kept, Transaction skipped (patched SDK) /ping yes yes

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 caveat that matters most

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.