Skip to main content

transparent-fastapi

CI Python License: MIT

Drop-in Prometheus metrics for FastAPI. One line of setup, no per-route changes, disciplined cardinality.

Why

FastAPI doesn't ship metrics, and the usual options leave gaps:

  • Hand-rolled middleware is easy to start and easy to get wrong — raw URLs as labels turn /users/123 and /users/124 into separate time series, and a port scanner can balloon series count overnight.
  • prometheus-fastapi-instrumentator covers HTTP nicely, but route-template grouping is opt-in and process-level signals (event-loop lag, threadpool saturation, BackgroundTasks) aren't in scope.

transparent-fastapi is the tool you reach for when you want one install(app) to give you HTTP metrics with safe-by-default cardinality, plus the runtime signals you need to debug "why is my async app slow" — event-loop lag, threadpool tokens, and BackgroundTasks outcomes — without per-route changes.

Install

pip install transparent-fastapi

Use

from fastapi import FastAPI
from transparent_fastapi import install

app = FastAPI()
install(app)

That's it. A /metrics endpoint is now exposed in Prometheus exposition format.

Call install(app) once, as early as possible after app = FastAPI(...). Any middleware you add afterwards (auth, CORS) wraps the metrics middleware, so route-handler latency isn't skewed by upstream concerns.

Compatibility: Python 3.10+, FastAPI 0.75+, asyncio (anyio default backend). Fully type-annotated — ships a py.typed marker.

Cardinality discipline

This is the whole point of the library — make it safe to leave running in production.

  • Routes are FastAPI's path templates (/users/{id}), never raw URLs. /users/123, /users/124, /users/125 collapse into one series.
  • Unmatched paths (port scans, vulnerability probes) collapse to route="<unmatched>" rather than leaking the raw path as a label.
  • Methods are allowlisted (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS); anything else becomes OTHER.
  • Status is the full numeric code ("200", "404"); fine-grained enough for "alert when 401 spikes vs 500 spikes" without exploding cardinality.

What it exports

Metric Type Labels
http_requests_total Counter method, route, status
http_request_duration_seconds Histogram method, route
http_requests_in_flight Gauge —
event_loop_lag_seconds Gauge —
threadpool_tokens Gauge state ∈ {total, borrowed, available}
threadpool_tasks_waiting Gauge —
background_task_scheduled_total Counter mode ∈ {async, threadpool}
background_task_total Counter mode ∈ {async, threadpool}, outcome ∈ {ok, error}
background_task_duration_seconds Histogram mode
Plus, free from prometheus_client's default registry

prometheus_client auto-registers a process collector and a Python collector. Because transparent-fastapi's /metrics endpoint calls generate_latest() on the default registry, these are exposed too — you don't need to do anything:

Metric Type Labels What it tells you
process_cpu_seconds_total Counter — CPU time consumed; rate() → cores in use
process_resident_memory_bytes Gauge — RSS (physical memory)
process_virtual_memory_bytes Gauge — VSZ
process_open_fds Gauge — FDs in use; creep toward process_max_fds = leak
process_max_fds Gauge — Soft FD limit
process_start_time_seconds Gauge — Unix epoch of process start; time() - this = uptime
python_gc_collections_total Counter generation ∈ {0, 1, 2} GC collections; sustained gen-2 = long-lived churn
python_gc_objects_collected_total Counter generation Objects reclaimed per generation
python_gc_objects_uncollectable_total Counter generation Objects GC could not free (cycles with __del__)
python_info Info version, implementation, major, minor, patchlevel Python build details

Configuration

Two optional kwargs:

install(
    app,
    excluded_paths=["/health", "/readiness"],   # noisy probes don't pollute metrics
    background_task_metrics=True,                # default; opt-out of the BackgroundTasks patch
)

Useful queries

# Validation-failure rate by route — Pydantic 422s already live in the status label
sum by (route) (rate(http_requests_total{status="422"}[5m]))

# 5xx error ratio
sum(rate(http_requests_total{status=~"5.."}[5m])) /
  sum(rate(http_requests_total[5m]))

# p95 latency by route
histogram_quantile(0.95,
  sum by (route, le) (rate(http_request_duration_seconds_bucket[5m])))

# Threadpool saturation — non-zero means sync work is queuing
sum(threadpool_tasks_waiting)

# Background task backlog growth (positive = scheduling outpaces completion)
sum(rate(background_task_scheduled_total[1m]))
  - sum(rate(background_task_total[1m]))

# Process uptime in seconds
time() - process_start_time_seconds

Limitations

FastAPI(root_path=...) and app.include_router(router, prefix=...) are handled correctly — both produce clean, prefixed templates in the route label. A couple of app.mount(...) cases are worth knowing about:

  • StaticFiles (and any Starlette Mount) records as route="<unmatched>". Starlette's Mount doesn't populate request.scope["route"] the way FastAPI's APIRoute does, so static-file requests share a single series with unmatched paths (port scans, typos). This is a feature for cardinality — a 100k-asset directory can't explode the time series — but it does mean static traffic isn't separately observable here. Per-asset metrics belong at the CDN / edge proxy.
  • Mounted FastAPI sub-applications share series with top-level routes. app.mount("/api", sub_app) records traffic as route="/users/{user_id}", without the /api prefix. This is fine when you have one mount; two mounts of the same sub-app at different paths would collide on the same series. Prefer app.include_router(router, prefix="/api") for FastAPI-internal composition — its templates are baked with the prefix and stay distinct.
How it works
  • HTTP metrics are recorded by two ASGI middlewares wired up by install(app). Route templates are read from request.scope["route"].path after dispatch, so labels are always the matched template — never a raw URL.
  • Event-loop lag is sampled by a background task install(app) adds to your lifespan. It schedules a 1-second sleep and reports the delta between expected and actual wake-up.
  • Threadpool gauges are refreshed at scrape time from anyio's default CapacityLimiter, so the values are always live, not polled.
  • Background task metrics come from a one-time monkey-patch of starlette.background.BackgroundTasks.add_task. Your route code keeps using background_tasks.add_task(...) as normal; the patch wraps the func to record ok/error and duration, and labels by mode using starlette's own async-vs-threadpool detection.

Local demo stack

The repo ships a docker-compose stack (app + Prometheus + Grafana with a provisioned dashboard) and locust load profiles under deploy/local/. From the project root:

docker compose -f deploy/local/docker-compose.yml up -d --build
deploy/local/scripts/deploy.sh status        # curl all three services
deploy/local/scripts/deploy.sh load-medium   # locust, realistic traffic mix

Grafana lands on http://localhost:3000 (admin/admin) with transparent-fastapi as the default dashboard. See deploy/local/CLAUDE.md for the full reference.

License

MIT

Metadata

Release files for transparent-fastapi 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for transparent-fastapi 0.1.2
File Size Uploaded
transparent_fastapi-0.1.2.tar.gz 155.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for transparent-fastapi 0.1.2
File Interpreter ABI Platform
transparent_fastapi-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 166.6 kB

Release files / transparent_fastapi-0.1.2.tar.gz

Download URL transparent_fastapi-0.1.2.tar.gz
Size 155.5 kB
Tags Source
SHA-256 checksum
How to use checksums
e0db2c3d04cbe4641658b22080019a09de62a32b1ad06f372f8c888b7b30575f
BLAKE2b-256 checksum
How to use checksums
dacb23ff97cb568c3411cf83f661b9ed9feed2661c8e697ab3e8493f477d0a53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 1, 2026.

Transparency log

Release files / transparent_fastapi-0.1.2-py3-none-any.whl

Download URL transparent_fastapi-0.1.2-py3-none-any.whl
Size 11.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f136856dce43f585212e6ade2b201ffbd277f025bd0aeafb8d9d6a42ab93afdd
BLAKE2b-256 checksum
How to use checksums
14f94993eb267561b37ef3dbdf3f299af0d63924c66f1f70588e7aae7930982e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page