Skip to main content

ASGI static file server — like Whitenoise, but for ASGI

Project description

whitesnout

WhiteSnout is an ASGI static file server for Python — like Whitenoise, but built for ASGI frameworks (FastAPI, Starlette, Django, etc.). It serves static files with minimal memory overhead, streaming content in chunks and leveraging pre-compressed assets. A Rust extension (PyO3) accelerates the hot path transparently.


Quick start

from whitesnout import WhiteSnout

# Standalone static file server
app = WhiteSnout(directory="./static")

Serve with any ASGI server:

$ uvicorn myapp:app

With FastAPI

from fastapi import FastAPI
from whitesnout import WhiteSnout

api = FastAPI()

@api.get("/api")
def read_root():
    return {"hello": "world"}

app = WhiteSnout(api, directory="static")

With Django

# asgi.py
import os
from django.core.asgi import get_asgi_application
from whitesnout import WhiteSnout

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

django_app = get_asgi_application()
application = WhiteSnout(django_app, directory="static")

Installation

$ uv add whitesnout

Or with pip:

$ pip install whitesnout

Requires Python ≥ 3.10.

The compress CLI needs Brotli:

$ uv add 'whitesnout[compress]'

Pre-compressing assets

Generate .gz and .br variants for all files in a directory:

$ python -m whitesnout compress static/
Compressed: 42 gzip, 42 brotli

This is a build-time step — at runtime WhiteSnout serves the pre-compressed files directly with zero CPU overhead.


Configuration

All options can be passed as keyword arguments to WhiteSnout:

Option Default Description
app None Inner ASGI app to fall through to when a file is not found
directory "static" Root directory to serve files from
index_file "index.html" File to serve for directory requests
cache_max_age 3600 max-age in Cache-Control for regular files
immutable_max_age 31536000 max-age for hashed files (1 year)
immutable_pattern r"\.[a-f0-9]{8,}\." Regex to detect hashed filenames (e.g. styles.a1b2c3d4.css)
chunk_size 65536 Stream chunk size in bytes (64 KB)
charset "utf-8" Charset for text-based content types
brotli True Look for .br pre-compressed variants
gzip True Look for .gz pre-compressed variants
max_cache_size 100 Max entries in the LRU stat cache
app = WhiteSnout(
    app=my_asgi_app,
    directory="public",
    cache_max_age=86400,
    immutable_max_age=31536000,
    chunk_size=131072,
)

Features

  • ASGI-native — middleware or standalone, works with any ASGI framework
  • Streaming — files are served in configurable chunks (64 KB by default), never loaded entirely into memory
  • Zero-copy pre-compression — serves pre-existing .gz and .br files with automatic Accept-Encoding negotiation; brotli preferred over gzip
  • Powerful cachingETag, Last-Modified, Cache-Control headers; 304 Not Modified responses for conditional requests
  • Immutable cache — detects hashed filenames (e.g. app.abc12345.js) and applies Cache-Control: public, immutable, max-age=31536000
  • Index filesindex.html served automatically for directory paths
  • Clean URLs/dir redirects to /dir/ (301 Moved Permanently)
  • Path traversal protection — resolved paths are verified to stay within the root directory
  • Low overhead — LRU cache for file stats reduces stat() syscalls; no dependency bloat
  • MIME types — content-type detection for 30+ file extensions, with automatic charset for text types
  • Compress CLIpython -m whitesnout compress <directory> generates pre-compressed .gz and .br files as a build step
  • Rust extensionwhitesnout._rs speeds up the LRU cache transparently; pure Python fallback when unavailable
  • Multi-platform wheels — pre-built for Linux (x86_64, arm64), macOS (x86_64, arm64), and Windows (amd64)

Architecture

whitesnout/
├── main.py              # ASGI middleware (always Python)
├── file_handler.py      # Path resolution, compression negotiation
├── response.py          # Header building, chunked streaming, 304
├── cache.py             # LRU cache (Python → falls back to Rust)
├── config.py            # Configuration dataclass
├── utils.py             # MIME type table, helpers
├── cli.py               # CLI entry point
├── compress.py          # Compression logic
└── py.typed

whitesnout._rs           # Compiled Rust extension (PyO3)
├── LRUCache             # Rust implementation, auto fallback to Python

The core logic consists of pure functions designed for gradual migration to Rust. The ASGI integration layer (main.py) stays in Python forever — it is the thin touchpoint with the ASGI protocol.


Development

With Docker (recommended)

$ make build     # Build Docker image + compile Rust + install deps
$ make test      # Run test suite inside container
$ make shell     # Open interactive shell in container
$ make release   # Build release wheel

Requires Docker. The image is based on rust:slim-trixie with Python, uv, and maturin pre-installed.

Without Docker

$ uv sync --dev               # Install Python deps + build Rust extension
$ uv run pytest -v            # Run tests
$ maturin develop --uv        # Rebuild Rust extension only

Requires Rust (via rustup) and maturin (cargo install maturin).


CHANGELOG

See CHANGELOG.md for the full release history.


Benchmark

Results measured with benchmarks/benchmark.py — 500 requests (10 concurrent) against uvicorn with a mix of static files (265 KB across 34 items) and a JSON API endpoint.

  • RPS — Requests per second (higher is better)
  • P50 — Median latency in milliseconds (lower is better)
  • P99 — 99th percentile latency in milliseconds (lower is better)
  • RAM — Resident set size in megabytes (lower is better)

v1.0.0 — Production readiness

Server RPS P50 (ms) P99 (ms) RAM (MB)
whitesnout 794 6.1 83.8 32.8
whitenoise 829 6.3 91.0 31.6

Platform: Linux x86_64 · Python: 3.14.3 · uvicorn: 0.47.0

v0.5.0 — CORS, Logging & Cache invalidation

Server RPS P50 (ms) P99 (ms) RAM (MB)
whitesnout 868 5.7 72.4 32.5
whitenoise 844 5.5 108.6 31.6

Platform: Linux x86_64 · Python: 3.14.3 · uvicorn: 0.47.0

v0.4.0 — Rust Phase 2 + O(1) LRU

Server RPS P50 (ms) P99 (ms) RAM (MB)
whitesnout 859 5.6 59.3 32.6
whitenoise 767 6.2 99.1 32.0

Platform: Linux x86_64 · Python: 3.14.3 · uvicorn: 0.47.0

v0.2.0

Server RPS P50 (ms) P99 (ms) RAM (MB)
whitesnout 966 5.8 119.2 31.8
whitenoise 925 5.9 80.0 31.5

Platform: Linux x86_64 · Python: 3.14.3 · uvicorn: 0.47.0

Running yourself

$ uv run python benchmarks/benchmark.py

ROADMAP

v1.0.0 ─── Env vars + Async file IO (done)
v0.5.0 ─── CORS + Logging + Cache invalidation
v0.4.0 ─── Rust Phase 2 + LRU O(1)
v0.3.0 ─── Range Requests + Security headers + Accept-Encoding
v0.2.0 ─── Ruff + Ty + type safety
v0.1.0 ─── Published
   │
   ├─ v0.2.0  Ruff + Ty + type safety
   ├─ v0.3.0  Range Requests + Security headers + Accept-Encoding quality values
   ├─ v0.4.0  Rust Phase 2 (utils, file_handler) + LRU cache O(1)
   ├─ v0.5.0  CORS + Logging + Cache invalidation
   └─ v1.0.0  Env vars + Async file IO + Benchmarks

### v0.2.0 — Ruff + Ty
- Add `ruff` (lint + format) and `ty` (type checker) for code validation
- Fix all lint/type errors; pass both in CI

### v0.3.0 — Range Requests & Security
- **Range Requests**: Parse `Range:` header, respond with `206 Partial Content` + `Content-Range`. Required for video, audio, and PDF seeking
- **Security headers**: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY` by default
- **Accept-Encoding quality values**: Parse `Accept-Encoding: gzip, br;q=0.1` correctly instead of naive substring matching
- **Respect brotli/gzip flags**: `find_compressed` must skip `.br` when `brotli=False`

### v0.4.0 — Rust Phase 2 + LRU O(1)
- Port `utils.py` (MIME types) → `src/utils.rs`
- Port `file_handler.py` (find_compressed, is_hashed_file) → `src/file_handler.rs`
- Replace `Vec`-based Rust LRU with `lru` crate (O(1) operations)
- Expand MIME type table from 30 → 100+

### v0.5.0 — CORS, Logging & Cache invalidation
- **CORS opt-in**: Config `cors=True` → add `Access-Control-Allow-Origin: *`
- **Logging**: Basic request logging (method, path, status, bytes, duration)
- **Cache invalidation**: Programmatic `invalidate()` method to purge the LRU cache

### v1.0.0 — Production readiness
- **Environment variables**: `WHITESNOUT_DIRECTORY`, `WHITESNOUT_CACHE_MAX_AGE`, etc.
- **Async file IO**: Migrate `iter_chunks` to `anyio` or `aiofiles` to avoid blocking the event loop
- **Benchmarks**: Compare against Whitenoise and raw ASGI serving

---

## License

MIT — see [LICENSE](LICENSE) for the full text.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

whitesnout-1.0.0.tar.gz (65.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

whitesnout-1.0.0-cp310-abi3-win_amd64.whl (783.1 kB view details)

Uploaded CPython 3.10+Windows x86-64

whitesnout-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

whitesnout-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl (1.0 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

whitesnout-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl (979.2 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ x86-64

whitesnout-1.0.0-cp310-abi3-manylinux_2_28_aarch64.whl (959.9 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ ARM64

whitesnout-1.0.0-cp310-abi3-macosx_11_0_arm64.whl (842.0 kB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

whitesnout-1.0.0-cp310-abi3-macosx_10_12_x86_64.whl (885.8 kB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file whitesnout-1.0.0.tar.gz.

File metadata

  • Download URL: whitesnout-1.0.0.tar.gz
  • Upload date:
  • Size: 65.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for whitesnout-1.0.0.tar.gz
Algorithm Hash digest
SHA256 f8201d5753341a691ce08e15b538942438b1c3324b7bb5771d9710bad5dba2a0
MD5 c023972ea09b8991927b21272c7b85f0
BLAKE2b-256 994eefbacc409954a5ed32e2345336caddf94ed213569c12409d87237be09480

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0.tar.gz:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: whitesnout-1.0.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 783.1 kB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 8ef9d996a418f02998c8d524dfee5c2d7aad76d8610969b47d01613b6a606a74
MD5 74d25c775a235812be285654064ab909
BLAKE2b-256 8654843474b4cbf33203a8fd02810aafc69e95ae01c69a0a5c49d54b0d315d73

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-win_amd64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 b3b016011f44172394ed64d810f7898dfa307dff8f553873b82ee1bb111dbff0
MD5 34281fc9f4e2f5a3d5956f999a3ed963
BLAKE2b-256 74a23c4469ef83ef1eacf10e9e7edb5a8f7d29b8ee69a65f7163cfb85d77fad7

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 ac0f3d340300ec41f96372f70b47275296254cca775226dcc9660fd4343d0938
MD5 c3395edf325f8f5e4333a1226998a032
BLAKE2b-256 81308b8ccf2fbaef0458c3ad68370820f76398e524ffa5aa319163c60659e6f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 afceb3e4b751a7002f69e72a2787ef29f2e9d2a981fc65a8498308e21c203da5
MD5 0bce6e4027359bf0cbeb03b610891053
BLAKE2b-256 2f7fe0f0930a447206911f8a30d9fcf12b30b04d72b949543ea00f4a065a7f11

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 9cc45d4fc5e9039d2931352c6c166764de0397ce8ef0185e1404fc4940eae563
MD5 0a4d42f84da24527823f5e1d90cc8e13
BLAKE2b-256 b5fcb52b46ca3516bfe05d86c53f1fa21cac37512b27eda121787d56929fe363

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-manylinux_2_28_aarch64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 471c5806ee3271667f1490e0f01dd2c7361bd12d9998e33cf1ba4cf17506cbdc
MD5 256ad11f40a34a7b3f8ce74de86b946c
BLAKE2b-256 d87c103521eba9ba4aa4cba7857ff1257f1bf5ba1adfffac9130904cacc6fcd6

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whitesnout-1.0.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-1.0.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 fa5debc77fa072f19e019dca162d60815e5b3a48c8c2daccde41549f6fb7132a
MD5 94ed26f9a20a5392e73a115a4b326cac
BLAKE2b-256 93b001f0b649f6ec61ad80fc1264d970d77351a8b2fd2ff124c2b3d75ab5237a

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-1.0.0-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: publish.yml on rroblf01/whitesnout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page