Granian tuning
Granian is the ASGI server running the PutFS API. The defaults work for most deployments; this page covers what matters for streaming uploads.
Note
Reads (GET/HEAD) are served by nginx via sendfile and never reach granian. Tuning granian only affects writes and listings.
Workers
One Python process each. Match the CPU core count, never more than 2x: the API is I/O-bound, so extra workers only add memory and context switching.
| CPU cores | Workers |
|---|---|
| 2 | 2 |
| 4 | 4 |
| 8 | 4–8 |
| 16+ | 8–16 |
Runtime mode
mt shares the Rust async runtime across workers and is the better fit on multi-core systems. st gives each worker its own runtime and only pays off with 1–2 workers.
Event loop
uvloop is the default. In our benchmarks it has ~20% lower per-request latency on small writes (1K–1M) than rloop; rloop's higher throughput on 10G+ sequential writes disappears behind real network I/O.
Backpressure
Maximum concurrent requests per worker. Each PUT is a long-lived connection, so this is how many uploads run at once. The default derives from backlog / workers.
| Workload | Backpressure |
|---|---|
| Few large uploads | 16–32 |
| Many small uploads | 128–256 |
| Mixed | 128 (PutFS default) |
Too high wastes memory (each in-flight request holds a buffer), too low queues requests.
HTTP/1 buffer size
Default is ~408KB. PutFS sets 10MB to match its streaming chunk size: fewer read syscalls per chunk.
Memory budget
The three knobs multiply. Worst case for an all-PUT workload:
With PutFS defaults:
That is the ceiling, reached only if every concurrent request is mid-upload with both buffers full; steady state is far lower. On a memory-constrained host, halve GRANIAN_BACKPRESSURE first, then reduce PUTFS_CHUNK_SIZE and GRANIAN_HTTP1_BUFFER_SIZE together (they should match). Cutting GRANIAN_WORKERS below the core count costs throughput; do it last.
Protocol
HTTP/1.1 only: nginx proxies via HTTP/1.1 anyway, and HTTP/2 adds framing for nothing. PutFS uses no WebSockets.
Unix socket
The Docker image listens on /run/putfs/putfs.sock:
TCP instead (slower):
Environment variables
Every setting is a GRANIAN_ env var. The Docker image sets these defaults:
GRANIAN_INTERFACE=asgi # don't change
GRANIAN_UDS=/run/putfs/putfs.sock
GRANIAN_UDS_PERMISSIONS=666
GRANIAN_WORKERS=4 # match CPU cores
GRANIAN_RUNTIME_MODE=mt
GRANIAN_LOOP=uvloop
GRANIAN_BACKPRESSURE=128
GRANIAN_HTTP1_BUFFER_SIZE=10485760
GRANIAN_HTTP=1
GRANIAN_NO_WS=true
What not to change
--blocking-threads: WSGI only; granian errors at startup under ASGI.--http 2: framing overhead, no benefit for streaming uploads.--runtime-mode st: only useful with 1–2 workers.--workers 1 --threads 16on Python 3.14t: free-threaded Python is slower here, with far higher write latency and memory. The GIL is not the bottleneck; PutFS is I/O bound and the GIL releases during I/O.