Sign in →
1 min read

Python SDK

Add metering to FastAPI, Django, and Flask with the Python SDK.

Updated 2026-11-11Suggest edits
Get the Python SDK
The Python SDK lives in the public repo github.com/aforoai/SDKs under aforo-metering-sdks/python.
View on GitHub Download ZIPgit clone https://github.com/aforoai/SDKs.git

Install & Quickstart#

Add Aforo metering to your Python service. FastAPI, Django, and Flask middleware plus a manual track() client.

aforo-metering 2.0.0 needs Python 3.9+; the only hard dependency is httpx>=0.25. track() puts the event on a bounded queue and returns; one background thread per client sends batches of up to 500.

terminal
pip install aforo-metering
pip install "aforo-metering[fastapi]"   # or [django] / [flask]

Manual Tracking

usage.py
import os
from aforo import AforoClient

client = AforoClient(api_key=os.environ["AFORO_API_KEY"], product_type="API")

client.track(
    customer_id="cust_1",      # who is billed
    metric_name="api_calls",   # what you're metering
    quantity=1,
)

# Ask the sender to send now and wait (returns early while the ingestor is down):
result = client.flush()        # FlushResult(sent=..., failed=...)
client.shutdown()              # flushes for at most shutdown_timeout; also runs via atexit

Framework Middleware

middleware.py
# FastAPI / Starlette -- the resolver receives the ASGI scope
from aforo.middleware.fastapi import AforoMeteringMiddleware
app.add_middleware(AforoMeteringMiddleware, api_key=os.environ["AFORO_API_KEY"],
                   customer_resolver=lambda scope: scope["user"].customer_id,  # set by your auth middleware
                   metric_name="api_calls")

# Flask -- the resolver receives the request
from flask import g
from aforo.middleware.flask import AforoMetering
AforoMetering(app, api_key=os.environ["AFORO_API_KEY"], metric_name="api_calls",
              customer_resolver=lambda req: g.current_user.customer_id)

# Django settings.py -- a callable or its dotted path
MIDDLEWARE = [..., "aforo.middleware.django.AforoMeteringMiddleware"]
AFORO_API_KEY = os.environ["AFORO_API_KEY"]
AFORO_CUSTOMER_RESOLVER = "aforo.middleware.django.user_id_resolver"  # the authenticated user's id
AFORO_METRIC_NAME = "api_calls"

Each adapter records api_calls unless you pass metric_name. customer_resolver is required: with neither it nor trust_customer_header=True (Django: AFORO_TRUST_CUSTOMER_HEADER = True, which reads X-Customer-Id), the middleware raises ValueError when your app starts. A request whose resolver returns nothing is served normally and counted as an invalid drop. The caller's X-Api-Key is never used as a customer id. Each middleware exposes its client as .client for flush() and shutdown().

Who Is Billed#

Every integration that meters an incoming request — HTTP middleware, the gRPC / GraphQL / WebSocket / MQTT clients, the MCP SDKs and the MCP proxy — needs to know which customer to bill, and from 2.0.0 you tell it. Pass a customer resolver that reads identity your service already verified (a session, a JWT subject, an API-key lookup, an mTLS identity).

  • Reading a value the caller of your API controls — the X-Customer-Id header, gRPC metadata, MCP _meta.customer_id, the MQTT username — needs an explicit opt-in: trustCustomerHeader (Node, Java), trust_customer_header (Python), TrustCustomerHeader (Go). Turn it on only when a proxy you control sets the value and strips any client copy.
  • With neither a resolver nor the opt-in, construction fails (Node throws, Python raises ValueError, Go returns an error, Spring fails to start). There is no fallback to unknown, anonymous or the agent id.
  • When the resolver returns nothing for one request, the request runs normally and its event is dropped as invalid and counted.
  • A manual track() call is unchanged: you pass customerId yourself.

Delivery & Dropped Events#

All 2.0.0 SDKs send the same way. track() only queues (bounded, 10,000 events by default; on overflow the oldest is evicted). One background sender per client sends one request at a time: every 5 s, or as soon as 500 events are waiting (the batch size, capped at 1,000). Retries on network errors, timeouts, 408, 429 and 5xx use full-jitter backoff and honour Retry-After (seconds or an HTTP date, capped at 60 s).

An outage does not lose usage. After the last retry — or on 401 / 403, which usually means a wrong or revoked key and is logged as an error — the batch goes back to the front of the queue with the same idempotency keys and a breaker pauses sending (5 s, doubling to 5 min). New events keep queueing; the first successful request resumes sending, and the ingestor deduplicates the re-sent keys. Usage is lost only if the queue overflows first, so raise the queue size if you expect long outages. A 413 splits the batch in half.

Shutdown is bounded: shutdown() / close() keeps sending for at most the shutdown timeout (5 s by default) and reports what is still queued. The libraries install no signal handlers — call shutdown from your own SIGTERM path. Python also flushes at atexit, Node on beforeExit, Java in Spring's stop phase.

Drop reasonWhen
overflowThe queue was full (usually during a long outage); the oldest event was evicted.
rejectedThe ingestor refused the batch with a non-retryable 4xx, refused individual events inside an accepted batch, or answered 413 to a single event. Unknown metric names land here.
invalidThe event failed a client-side check before it was queued (blank customer or metric, quantity <= 0, a field over the ingestor limit), or a customer resolver returned nothing.
shutdownStill queued when the shutdown timeout ran out.
closedtrack() was called after shutdown. It does not throw (Go returns ErrClientClosed).
retry_exhaustedKept for API compatibility only — an outage requeues instead.

Every dropped event is counted, logged, and passed to the opt-in onDrop(events, reason) hook (on_drop in Python, OnDrop in Go). Events keep their idempotency keys, so re-tracking them after recovery does not bill twice.

See also
Aforo Documentation — Aforo Docs