🔧 LeakySpanner
Zero-interference memory leak profiler for FastAPI.
Pure spectator mode — watches everything, touches nothing.
The Problem
FastAPI + Python async apps can silently grow to 4–5 GB RAM at peak load.
Finding where is painful because the leak could be anywhere — images held in memory,
uncleaned async tasks, connection pools, Gemini API response objects.
The Solution
LeakySpanner attaches to your FastAPI app as a pure spectator:
- ✅ Watches every request
- ✅ Samples memory in a background thread every N seconds
- ✅ Tracks memory delta per endpoint
- ✅ Uses
tracemallocto find top allocation sites - ✅ Writes a clean log file you can grep through
- ❌ Never blocks a request
- ❌ Never adds latency
- ❌ Never modifies request/response
- ❌ If LeakySpanner crashes — your app keeps running
Install
pip install leakyspanner
Usage
from fastapi import FastAPI
from leakyspanner import leakyspanner
app = FastAPI()
# One line — that's it
leakyspanner(app)
With options:
leakyspanner(
app,
enabled=True, # set False in tests
log_path="./leak_logs", # where to write logs
level="deep", # basic | mid | deep
interval_seconds=5.0, # memory snapshot every 5s
leak_threshold_mb=50.0, # warn if RSS grows 50MB above baseline
)
Levels
| Level | What it tracks |
|---|---|
basic |
Endpoint-level memory delta per request |
mid |
Endpoint deltas + tracemalloc top allocation stats (default) |
deep |
Everything + per-snapshot top allocation traces with file/line |
Log Output
LeakySpanner writes to ./leakyspanner_logs/leakyspanner_YYYYMMDD_HHMMSS.log
Snapshot log (every 5s):
[14:32:05] RSS=312.4MB VMS=890.1MB delta=+12.1MB GC=(142, 8, 1)
[14:32:10] RSS=318.7MB VMS=890.1MB delta=+18.4MB GC=(198, 8, 1)
Endpoint delta log (when delta > 1MB):
[14:32:07] ENDPOINT POST /ocr/process mem_delta=+6.23MB status=200
[14:32:08] ENDPOINT POST /ocr/process mem_delta=+5.91MB status=200
Warning (when growth > threshold):
⚠️ [14:35:00] MEMORY GROWTH ALERT: +87.3 MB above baseline
(baseline=312.4 MB, current=399.7 MB)
Session summary (on shutdown):
======================================================================
LEAKYSPANNER SESSION SUMMARY
======================================================================
Peak RSS : 4821.3 MB
Final RSS : 4103.7 MB
Baseline RSS: 312.4 MB
Net growth : +3791.3 MB
ENDPOINT MEMORY DELTAS (sorted by avg):
POST /ocr/process calls= 412 avg=+8.73MB max=+41.20MB
GET /health calls=1204 avg=+0.00MB max= +0.01MB
======================================================================
Deep level — tracemalloc traces:
── top allocations ──
42.3KB × 18 /app/services/gemini.py:87
38.1KB × 412 /app/services/ocr.py:134
21.7KB × 1 /usr/lib/python3.11/ssl.py:1092
Reading the Logs
Once you have the log, look for:
- Endpoint with highest
avgdelta → that's your primary suspect - Steadily rising RSS snapshots that never drops → classic leak
- tracemalloc lines pointing to specific file:line → that's the smoking gun
- GC counts growing without dropping → reference cycles not being collected
Real-world Example (OCR App)
app = FastAPI()
leakyspanner(
app,
level="deep",
log_path="/var/log/myapp/leaks",
leak_threshold_mb=200.0, # alert at 200MB growth
interval_seconds=10.0, # sample every 10s in prod
)
@app.post("/ocr/process")
async def process(file_id: str):
# LeakySpanner watches this — you change nothing
results = await asyncio.gather(
call_gemini_1(file_id),
call_gemini_2(file_id),
call_qr_service(file_id),
fetch_from_db(file_id),
)
return results
License
MIT
Metadata
Release files for leakyspanner 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| leakyspanner-0.1.2.tar.gz | 17.2 kB | Without provenance | Details for leakyspanner-0.1.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| leakyspanner-0.1.2-py3-none-any.whl | Python 3 | none | any | Details for leakyspanner-0.1.2-py3-none-any.whl |
Total release size: 29.2 kB
Release files / leakyspanner-0.1.2.tar.gz
| Download URL | leakyspanner-0.1.2.tar.gz |
|---|---|
| Size | 17.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0cb81e4eeecf931fa0ad28f0c8f1e4ab50736d4daac72de10011ec303b7c8189
|
|
BLAKE2b-256 checksum How to use checksums |
98a011ababb2043d41273592f91aee030fc840a73b1e56189025c46972b22fcb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|
Release files / leakyspanner-0.1.2-py3-none-any.whl
| Download URL | leakyspanner-0.1.2-py3-none-any.whl |
|---|---|
| Size | 12.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef1205410d9f5bab73e03809da2e3d6d35b41e936adcf154896ee0e8053c4577
|
|
BLAKE2b-256 checksum How to use checksums |
afea220ece6f06221bd104a8c03cc7908d1438de44b22de9a3d0d134929e0f32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|
Security for leakyspanner 0.1.2
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.