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)

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

v0.5.0 ─── CORS + Logging + Cache invalidation (current)
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 a LinkedHashMap for O(1) operations
  • Add Rust unit tests (#[cfg(test)])
  • 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 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-0.4.0.dev0.tar.gz (63.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-0.4.0.dev0-cp310-abi3-win_amd64.whl (782.2 kB view details)

Uploaded CPython 3.10+Windows x86-64

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

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

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

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

whitesnout-0.4.0.dev0-cp310-abi3-manylinux_2_28_x86_64.whl (978.3 kB view details)

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

whitesnout-0.4.0.dev0-cp310-abi3-manylinux_2_28_aarch64.whl (959.3 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ ARM64

whitesnout-0.4.0.dev0-cp310-abi3-macosx_11_0_arm64.whl (840.9 kB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

whitesnout-0.4.0.dev0-cp310-abi3-macosx_10_12_x86_64.whl (885.0 kB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file whitesnout-0.4.0.dev0.tar.gz.

File metadata

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

File hashes

Hashes for whitesnout-0.4.0.dev0.tar.gz
Algorithm Hash digest
SHA256 cbd30f06251e7c8f627165b44618563dfcbc5aae4af39fd79cefa2a99d520664
MD5 c47c6e04ec34a329c14c58447589aa88
BLAKE2b-256 448bc17938336f72fb4aa7f68494d9211f7496a11a644def842310487365415c

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0.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-0.4.0.dev0-cp310-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 d7a682fafe9e2f507c18cd8749308ca49e2aef3bd4e2d6a20820fd2067108744
MD5 86495649dc3bbd394e369a6b3d876591
BLAKE2b-256 49f3f691f06f2ff111c133d83dd764d3fdb6ba4e4129b6f4faead75b931bb696

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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-0.4.0.dev0-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 8fd4e719f2bce79204006b7bc8da5dc1dcd5bebdf68b1f4643fc7ac222147660
MD5 7b87c087ed19894e9e4ae70074652cff
BLAKE2b-256 b9d54ded09647e32f016c66281e40cb7eb8accf36cb6b2a3813f3b83e8784610

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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-0.4.0.dev0-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 6b472dc4bf8b0b8fcbb493819e385ba531bc135147faaf79edfa44dbcba7e737
MD5 85e41d032e6d9b573032b0d503df8375
BLAKE2b-256 01a36bdcb2baa15acf3059ab5dddfba20c3ca4d2f395d7c2e64cf06b7690d9e8

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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-0.4.0.dev0-cp310-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 f84b4cbeea8ab47646a37e1ed282bd7b5783f14cefab5dc2e90bc784c11a49b1
MD5 97d09d27eb0649d7f0b418d536eb299a
BLAKE2b-256 2ea96f1d5408f71b459714c07efc334dbb73e6919f981a7b77949d5c95d39803

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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-0.4.0.dev0-cp310-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 84875f4717b70333cde29e75da28e60d6b94b69aab51075bd6395a4027e6e0a0
MD5 e854629a7e2eae7f2b1a587ec9806719
BLAKE2b-256 9d60186d4cbc8e4619ca9ed5d6052b26ee89a4263fcd8c685cace0f45d6ea3bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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-0.4.0.dev0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ce4663475aaebbbd033550d2c4efca5e5a742062dd162fc1847557376c24a1ae
MD5 553c6cfd1317eba0e186e54ba3b2639e
BLAKE2b-256 2bcabd37dae9fc93152dda5a2bd955b650677a351885b9be84a27cb78c23345f

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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-0.4.0.dev0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-0.4.0.dev0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 faa9656a544fe17cabfd5d3f753bf0fff7e134b20087baa65b3c5c870b247d86
MD5 1a4388869e166c474b2b0c2b4f3c1f8c
BLAKE2b-256 ca22f38186af1a29fba57ae6c84aa2ac8781e8fdd05791eaad7487a3a0572b05

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.4.0.dev0-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