Python SDK¶
Python 3.9–3.13. One runtime dependency (httpx). PyPI · Changelog
Instrument your framework¶
reqly.instrument(app) detects the framework itself. There are no decorators and no middleware to wire up.
import reqly
from fastapi import FastAPI
app = FastAPI()
reqly.instrument(
app,
service_name="checkout-api",
collector_url="https://reqly.example.com",
api_key="your-ingest-key",
)
Routes in app.mount()ed sub-apps are included.
Django has no app object, so add the middleware first in MIDDLEWARE. This also works with Django REST Framework and Django Ninja.
For Bottle, Pyramid, Falcon, CherryPy or a bare ASGI app, wrap the app and serve the result. Only the framework knows the route template, so pass a route_resolver. Without one, every request is recorded as __unmatched__, never as a raw path.
Routes are recorded as templates in one style across frameworks: Django's users/<int:pk>/ and DRF's ^users/(?P<pk>[^/.]+)/$ both become /users/{pk}/.
What is recorded¶
For each request:
- method
- route template (
/orders/{id}, never/orders/123) - status code and duration
- error type
- host
- release and environment
- request and response body sizes
- optionally, a hashed consumer id and LLM token usage
Request bodies, query strings and headers are never sent.
Releases are detected automatically from REQLY_RELEASE or your CI or host: GITHUB_SHA, CI_COMMIT_SHA, RENDER_GIT_COMMIT, VERCEL_GIT_COMMIT_SHA, RAILWAY_GIT_COMMIT_SHA, HEROKU_SLUG_COMMIT, K_REVISION and others. Deploys then show up in Reqly with no extra code.
Who is calling¶
Tag each request with its API consumer. The id is hashed with HMAC-SHA256 and your secret salt before it leaves the app, so raw keys never reach the collector.
reqly.instrument(app, consumer_header="X-API-Key", consumer_salt=os.environ["REQLY_CONSUMER_SALT"])
# or any logic, from a RequestInfo:
reqly.instrument(app, consumer=lambda info: info.headers.get("x-tenant-id"))
See API consumers.
LLM cost per route¶
Record token usage where you call a model. The collector prices it per route.
completion = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
reqly.record_llm_response(completion) # OpenAI- or Anthropic-style responses, or:
reqly.record_llm_usage("gpt-4o-mini", input_tokens=1200, output_tokens=240)
See LLM cost.
Upload your OpenAPI spec¶
The SDK uploads your spec on the first request, so the dashboard can compare it with real traffic (OpenAPI drift). FastAPI and Litestar generate their own; any other app passes it, as a dict or a function returning one:
reqly.instrument(app, push_openapi=True) # FastAPI, Litestar
reqly.instrument(app, push_openapi=lambda: swagger.get_apispecs()) # Flask (flasgger)
app = reqly.instrument_wsgi(app, push_openapi=spec_dict, route_resolver=...)
# Django settings.py (e.g. drf-spectacular's generated schema)
REQLY = {"service_name": "checkout-api", "push_openapi": load_schema}
Configuration¶
Every option can be passed to instrument() or set as an environment variable. Resolution order: argument → environment variable → default.
| Argument | Environment variable | Default |
|---|---|---|
service_name |
REQLY_SERVICE_NAME |
sys.argv[0] basename |
collector_url |
REQLY_COLLECTOR_URL |
http://localhost:8000 |
api_key |
REQLY_API_KEY |
None |
release |
REQLY_RELEASE, then CI variables |
auto-detected, else None |
environment |
REQLY_ENVIRONMENT |
None |
sample_rate |
REQLY_SAMPLE_RATE |
1.0 |
flush_interval_seconds |
REQLY_FLUSH_INTERVAL_SECONDS (or REQLY_FLUSH_INTERVAL_MS) |
5.0 (at least 0.1); a full batch is sent at once |
max_batch_size |
REQLY_MAX_BATCH_SIZE |
200 (kept within 1–1,000, the collector's limit) |
max_queue_size |
REQLY_MAX_QUEUE_SIZE |
2000 (at least 1) |
ignore_routes |
REQLY_IGNORE_ROUTES (comma-separated) |
/health,/metrics |
consumer_header |
REQLY_CONSUMER_HEADER |
None, e.g. X-API-Key |
consumer |
— | None: callable(RequestInfo) -> str | None |
consumer_salt |
REQLY_CONSUMER_SALT |
None: set it |
hash_consumer |
REQLY_HASH_CONSUMER |
True. False sends ids unhashed (only for non-secret ids) |
push_openapi |
REQLY_PUSH_OPENAPI |
False. True on FastAPI and Litestar, or the spec (a dict or a function returning it) on any app |
With sample_rate below 1.0, request counts in the dashboard are the sampled volume; latency percentiles and error rates stay unbiased.
Guarantees¶
- Small overhead: about 13 µs per request on FastAPI and Starlette and 33 µs on Flask (benchmark).
- Fail-open: any internal SDK error is caught and logged once. Instrumentation disables itself rather than raise into your app.
- Non-blocking: shipping happens on a background thread with strict HTTP timeouts, so a slow or unreachable collector never blocks a request.
- Bounded cardinality: route templates only. Unmatched paths (404s, scanners) collapse into one
__unmatched__bucket. - Bounded memory: a fixed-size queue. Under backpressure the oldest events are dropped and counted.
- Safe retries: batches are retried with exponential backoff on
408,429and5xx. Other4xxresponses are dropped. Every event carries anevent_idthe collector deduplicates on, so a retry never double-counts. - Bounded exit: queued events are sent when the process exits, with one attempt per batch and at most 5 seconds in total, so a collector outage never holds up a deploy or a worker restart. What couldn't be sent is counted as dropped.
- Abandoned requests: a request cancelled before the app answered (the client disconnected, or the server is shutting down) is recorded as
499(client closed request), not as a500. - Pre-fork servers: under gunicorn
--preload(or uWSGI without lazy-apps), each worker restarts its own flush thread and connection pool.
Compatibility¶
| Supported | |
|---|---|
| Python | 3.9 – 3.13 |
| FastAPI | 0.100+, including routes in app.mount()ed sub-apps |
| Starlette | 0.27+, including Mount |
| Litestar | 2.0+ |
| Flask | 2.3+ |
| Django | 4.2+, sync and async views; DRF and Django Ninja |
| Other WSGI / ASGI | any, with instrument_wsgi() / instrument_asgi() and a route_resolver |
| Collector | any version. release, environment and body sizes need 0.3.0+; push_openapi needs 0.7.0+; consumer and LLM views need 0.8.0+ |