Drop-in Prometheus metrics for FastAPI: HTTP requests, latency, event-loop lag, threadpool tokens, background tasks. One line of setup, no per-route changes.
Project description
transparent-fastapi
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/123and/users/124into separate time series, and a port scanner can balloon series count overnight. prometheus-fastapi-instrumentatorcovers 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/125collapse 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 StarletteMount) records asroute="<unmatched>". Starlette'sMountdoesn't populaterequest.scope["route"]the way FastAPI'sAPIRoutedoes, 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 asroute="/users/{user_id}", without the/apiprefix. This is fine when you have one mount; two mounts of the same sub-app at different paths would collide on the same series. Preferapp.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 fromrequest.scope["route"].pathafter 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 usingbackground_tasks.add_task(...)as normal; the patch wraps the func to recordok/errorand 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file transparent_fastapi-0.1.2.tar.gz.
File metadata
- Download URL: transparent_fastapi-0.1.2.tar.gz
- Upload date:
- Size: 155.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e0db2c3d04cbe4641658b22080019a09de62a32b1ad06f372f8c888b7b30575f
|
|
| MD5 |
b36cbb5e2be2498a459e5055d7b2fb8e
|
|
| BLAKE2b-256 |
dacb23ff97cb568c3411cf83f661b9ed9feed2661c8e697ab3e8493f477d0a53
|
Provenance
The following attestation bundles were made for transparent_fastapi-0.1.2.tar.gz:
Publisher:
release.yml on Ashish-Github193/transparent-fastapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
transparent_fastapi-0.1.2.tar.gz -
Subject digest:
e0db2c3d04cbe4641658b22080019a09de62a32b1ad06f372f8c888b7b30575f - Sigstore transparency entry: 1417093684
- Sigstore integration time:
-
Permalink:
Ashish-Github193/transparent-fastapi@3a617ec11c43cc99d10f8d8a0188427e442af397 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/Ashish-Github193
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a617ec11c43cc99d10f8d8a0188427e442af397 -
Trigger Event:
push
-
Statement type:
File details
Details for the file transparent_fastapi-0.1.2-py3-none-any.whl.
File metadata
- Download URL: transparent_fastapi-0.1.2-py3-none-any.whl
- Upload date:
- Size: 11.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f136856dce43f585212e6ade2b201ffbd277f025bd0aeafb8d9d6a42ab93afdd
|
|
| MD5 |
085f2fc365f122cd1dfde7418e9bc9f8
|
|
| BLAKE2b-256 |
14f94993eb267561b37ef3dbdf3f299af0d63924c66f1f70588e7aae7930982e
|
Provenance
The following attestation bundles were made for transparent_fastapi-0.1.2-py3-none-any.whl:
Publisher:
release.yml on Ashish-Github193/transparent-fastapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
transparent_fastapi-0.1.2-py3-none-any.whl -
Subject digest:
f136856dce43f585212e6ade2b201ffbd277f025bd0aeafb8d9d6a42ab93afdd - Sigstore transparency entry: 1417093691
- Sigstore integration time:
-
Permalink:
Ashish-Github193/transparent-fastapi@3a617ec11c43cc99d10f8d8a0188427e442af397 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/Ashish-Github193
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a617ec11c43cc99d10f8d8a0188427e442af397 -
Trigger Event:
push
-
Statement type: