v3.9.1
Highlights
Conditional requests (HTTP caching). Set resp.etag or resp.last_modified and responder answers matching requests with a bodyless 304 Not Modified automatically:
@api.route("/report")
def report(req, resp):
resp.etag = compute_version_hash()
resp.text = expensive_render() # cached clients get a 304Implements RFC 7232 semantics: If-None-Match precedence over If-Modified-Since, weak-ETag comparison, * matching, GET/HEAD only.
Multi-process rate limiting. RateLimiter now accepts pluggable backends — the in-memory default keeps its sliding-window behavior, and the new RedisBackend shares one budget across all workers:
limiter = RateLimiter(
requests=100, period=60,
backend=RedisBackend(url="redis://localhost:6379/0"),
)Streaming uploads. async for chunk in req.stream() iterates large request bodies without buffering them in memory.
Application state. api.state is a free-form namespace reachable from handlers via req.api.state — and req.api is now actually populated with the owning API instance (it was always None before).
Fixed
API(static_dir=None)— a documented configuration — crashed on every route registration. The static-fallback check now only applies when no endpoint is given.
Performance
- Request headers are parsed lazily on first access: ~5% faster dispatch on header-heavy requests that never read them.
Full changelog: https://github.com/kennethreitz/responder/blob/main/CHANGELOG.md