- Use preexec_fn=os.setsid to create new process group
- Send signals to process group with os.killpg() instead of single process
- Add explicit timeout and graceful-timeout to gunicorn command
- Fixes test failures on PyPy 3.10 where signals weren't propagating properly
Replace signal-based dirty add/remove with protocol messages:
- Add MSG_TYPE_MANAGE to dirty protocol for worker management
- Add MANAGE_OP_ADD and MANAGE_OP_REMOVE operation codes
- Add handle_manage_request() in DirtyArbiter
- Update handlers to send messages instead of SIGTTIN/SIGTTOU signals
New workers only load apps that haven't reached their worker limits.
When all apps are at their limits, returns reason in response.
Only increment num_workers when a worker is actually spawned.
Add MSG_TYPE_STATUS to dirty protocol to allow querying the dirty
arbiter for its workers. The control socket now connects to the
dirty arbiter socket to retrieve worker information.
Add a control socket server and CLI client for runtime management
of Gunicorn instances, similar to birdc for BIRD routing daemon.
Features:
- Control socket server running in arbiter process (asyncio/threaded)
- gunicornc CLI with interactive and single-command modes
- JSON protocol with length-prefixed framing
- Commands: show workers/stats/config/listeners/dirty, worker add/remove/kill,
dirty add/remove, reload, reopen, shutdown
- Stats tracking (uptime, workers spawned/killed, reloads)
- Configurable socket path and permissions
New config options:
- control_socket: Unix socket path (default: gunicorn.ctl)
- control_socket_mode: Socket permissions (default: 0o600)
- --no-control-socket: Disable control socket
Add support for SIGTTIN and SIGTTOU signals to the dirty arbiter,
allowing dynamic scaling of dirty workers at runtime without restarting
gunicorn.
Changes:
- Add TTIN/TTOU to DirtyArbiter.SIGNALS
- Add num_workers instance variable for dynamic count
- Add _get_minimum_workers() to enforce app worker constraints
- Add signal handlers for TTIN (increase) and TTOU (decrease)
- Update manage_workers() to use dynamic count
- Add documentation for dynamic scaling
- Add unit tests for signal handling
- Add Docker integration tests
The minimum worker constraint ensures TTOU cannot reduce workers below
what apps require (e.g., if an app has workers=3, minimum is 3).
Closes#3489
* feat(dirty): add stash - global shared state between workers
Add a simple key-value store (stash) that allows dirty workers to share
state through the arbiter. Tables are stored directly in arbiter memory
for fast access and simplicity.
Features:
- Auto-create tables on first access
- Dict-like interface via stash.table()
- Pattern matching for keys (glob patterns)
- Module-level API: stash.put(), stash.get(), stash.delete(), etc.
Usage:
from gunicorn.dirty import stash
stash.put("sessions", "user:1", {"name": "Alice"})
user = stash.get("sessions", "user:1")
# Or dict-like
sessions = stash.table("sessions")
sessions["user:1"] = {"name": "Alice"}
New files:
- gunicorn/dirty/stash.py - Client API and StashTable class
- Protocol additions for MSG_TYPE_STASH and STASH_OP_* codes
Note: Tables are ephemeral - lost if arbiter restarts.
* test(dirty): add tests for stash protocol and encoding
Test coverage for:
- Stash message creation and encoding
- Protocol constants (MSG_TYPE_STASH, STASH_OP_*)
- Error classes (StashError, StashTableNotFoundError, StashKeyNotFoundError)
- StashTable dict-like interface
- Edge cases: unicode, complex values, special patterns
* example(dirty): add stash usage example and integration tests
- Add SessionApp to dirty_app.py demonstrating stash usage
- Add /session/* endpoints to wsgi_app.py
- Add test_stash_integration.py with Docker tests
- Update docker-compose.yml with stash-test service
- Fix: Set GUNICORN_DIRTY_SOCKET in dirty arbiter for worker access
* docs(dirty): add stash documentation
JSON serializes all dict keys as strings, so for compatibility the TLV
encoder should do the same. This fixes an error when tasks return dicts
with integer keys (e.g., aggregation results grouped by numeric ID).
Update client and streaming tests to work with the binary protocol:
- Update MockStreamWriter/MockStreamReader to use BinaryProtocol
- Replace string request IDs with integers
- Update test assertions to decode binary protocol messages
- Use HEADER_SIZE and decode_header/decode_message instead of old API
Update arbiter tests to work with the binary protocol:
- Update MockStreamWriter to decode binary messages
- Import binary protocol constants from module level
Update worker tests to work with the binary protocol:
- Use integer request IDs instead of strings
- Update MockStreamWriter to decode binary messages
- Import binary protocol constants from module level
Replace JSON-based protocol with binary format using 16-byte header:
- Magic bytes (GD), version, message type, payload length, request ID
- TLV-encoded payloads for efficient binary data transfer
- No base64 encoding needed for binary data
- Backwards compatible API (DirtyProtocol alias, dict-based interface)
Header format inspired by OpenBSD msgctl/msgsnd.
- Bump version to 25.0.2
- Update copyright year to 2026 in LICENSE and NOTICE
- Add license headers to all Python source files
- Add changelog entry for 25.0.2
- Fix nginx config to use keepalive with upstream (was sending
Connection: close which caused premature connection closure)
- Add _safe_write() to handle socket errors (EPIPE, ECONNRESET,
ENOTCONN) gracefully when client disconnects
- Fix ASGI scope server/client to always be 2-tuples for IPv6
compatibility (IPv6 sockets return 4-tuples)
- Add write_eof() before close() to ensure buffered data is flushed
- Bind to [::] for dual-stack IPv4/IPv6 support in test containers
Closes#3484
When a client disconnects during an ASGI request, the worker now:
1. Sends http.disconnect message to the app's receive queue
2. Allows a configurable grace period for cleanup (default: 3 seconds)
3. Only cancels the task after the grace period expires
This follows the ASGI HTTP Connection Scope spec which defines
http.disconnect as the message apps should receive when clients
disconnect: https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event
The grace period prevents CancelledError from propagating to async
database operations, allowing SQLAlchemy and other async DB libraries
to properly reset their connection pools.
New config option: --asgi-disconnect-grace-period (default: 3 seconds)
Closes#3482
The dirty module (which uses asyncio and concurrent.futures) was being
imported at gunicorn startup via gunicorn.arbiter. This caused
concurrent.futures to be imported before user code could call
gevent.monkey.patch_all(), breaking gevent's monkey-patching.
Changes:
- gunicorn/arbiter.py: Import DirtyArbiter and set_dirty_socket_path
lazily inside spawn_dirty_arbiter() instead of at module level
- gunicorn/dirty/worker.py: Import ThreadPoolExecutor lazily inside
run() method instead of at module level
- Add tests/workers/test_gevent_import_order.py with 5 tests verifying:
- concurrent.futures is NOT imported when gunicorn.arbiter loads
- gevent patching works correctly with gunicorn
- Reproduces the exact scenario from the bug report gist
This ensures gevent's monkey.patch_all() can run before concurrent.futures
is imported, allowing proper patching of threading primitives.
- Fix path expectation in test_scope_path_preserved (router strips /http prefix)
- Fix lifespan state check to use scope_state instead of module_state
- Add tolerance for partial failures in proxy concurrent test
- Add retry logic with proper assertions in HTTPS proxy FastAPI test
- Add get_app_workers_attribute() to read workers class attribute
- Update _parse_app_specs() to check class attribute when no config override
- Add Docker-based e2e tests for per-app worker allocation
- Add test apps: HeavyModelApp (workers=2), LightweightApp
- Add unit tests for get_app_workers_attribute function
- Add integration tests for class attribute detection
Allow dirty apps to specify how many workers should load them, enabling
significant memory savings for heavy applications like ML models.
- Add `workers` class attribute to DirtyApp (None = all workers)
- Add `parse_dirty_app_spec()` to parse "module:Class:N" format
- Add `DirtyNoWorkersAvailableError` for app-specific error handling
- Update DirtyArbiter with per-app worker tracking and routing
- Maintain backward compatibility when no dirty_apps configured
Example: 8 workers x 10GB model = 80GB RAM needed
With workers=2: 2 x 10GB = 20GB RAM (75% savings)
Configuration formats:
- Class attribute: `workers = 2` on DirtyApp subclass
- Config format: `module:class:N` (e.g., `myapp.ml:HugeModel:2`)
- Add explicit do_handshake() in base_async.py before ALPN check
when do_handshake_on_connect is False
- Mark eventlet worker as deprecated (removal in 26.0)
- Add HTTP/2 gevent example with Docker and tests
- Update documentation to reflect eventlet deprecation
- Remove eventlet websocket example (gevent version exists)
The ALPN fix ensures HTTP/2 works correctly with gevent and eventlet
workers when do_handshake_on_connect config is False (the default).
Without explicit handshake, selected_alpn_protocol() returns None.
- Send GOAWAY with correct error codes for protocol violations
- Handle StreamClosedError and FlowControlError gracefully
- Return False instead of raising for missing/closed streams
- Handle flow control window overflow per RFC 7540
- Fix reader race condition and add h2 exception handling
- Wait for WINDOW_UPDATE when flow control window is zero/negative
- Use h2 exception's error_code for INITIAL_WINDOW_SIZE violations
Implement HTTP 103 Early Hints as modern replacement for HTTP/2 Server Push.
This allows servers to send resource hints before the final response,
enabling browsers to preload assets in parallel.
WSGI support:
- Add wsgi.early_hints callback to environ dict
- Apps can call environ['wsgi.early_hints'](headers) to send 103 responses
- Silently ignored for HTTP/1.0 clients (don't support 1xx responses)
ASGI support:
- Handle http.response.informational message type
- Apps can await send({"type": "http.response.informational", "status": 103, ...})
HTTP/2 support:
- Add send_informational() method to HTTP2ServerConnection
- Add async send_informational() method to AsyncHTTP2Connection
- Wire up early hints in gthread worker for HTTP/2 requests
Includes unit tests and Docker integration tests for all protocols.
Tests the ASGI worker with direct HTTP requests without uWSGI protocol.
Includes tests for GET, POST, query strings, path handling, keepalive,
large bodies, and custom headers.
Add uWSGI protocol support to ASGI worker
- Implements AsyncUWSGIRequest class extending sync UWSGIRequest to reuse parsing logic with async I/O
- ASGI protocol handler selects between HTTP and uWSGI based on --protocol config option
- Allows gunicorn's ASGI worker to receive requests from nginx using uwsgi_pass directive
- Includes unit tests and Docker integration tests
Add comprehensive Docker integration tests verifying dirty arbiter
lifecycle under realistic conditions:
- Parent death detection via ppid monitoring
- Orphan cleanup on restart
- Dirty arbiter respawning after crash
- Graceful shutdown with SIGTERM
Also fix race condition in manage_workers() by checking self.alive
before spawning new workers during shutdown.
Add ppid monitoring to dirty arbiter's worker monitor loop. If the
main arbiter dies unexpectedly (SIGKILL, crash, OOM), the dirty
arbiter detects the parent change and shuts itself down gracefully.
This complements the existing orphan cleanup on startup.
- Close coroutines in mocked asyncio.run to prevent "never awaited" warning
- Fix flaky integration tests with proper async cleanup and try/finally
- Add uvloop to testing dependencies so uvloop test runs
- Add pytest warning filter for eventlet/asyncio incompatibility
When the main arbiter crashes and restarts, orphaned dirty arbiters
may continue running. This adds detection and cleanup:
- Add well-known PID file location based on proc_name
- Dirty arbiter writes PID on startup, removes on exit
- Main arbiter checks for orphans on fresh start (not USR2)
- Uses self.proc_name for USR2 compatibility (myapp vs myapp.2)
During USR2 upgrade, old and new dirty arbiters coexist with
separate PID files, preventing the old from removing the new's file.
Add support for streaming responses when dirty app actions return
generators (sync or async). This enables real-time delivery of
incremental results for use cases like LLM token generation.
Features:
- Streaming protocol with chunk/end/error message types
- Worker support for sync and async generators
- Arbiter forwarding of streaming messages
- Deadline-based timeout handling
- Async client streaming API
Protocol:
- Chunk messages (type: "chunk") contain partial data
- End messages (type: "end") signal stream completion
- Error messages can occur mid-stream
New files:
- benchmarks/dirty_streaming.py: Streaming benchmark suite
- tests/dirty/test_*_streaming*.py: Streaming test coverage
- docs/content/dirty.md: Streaming documentation with examples
Add tests to verify that when multiple dirty apps are configured,
messages are correctly routed to the appropriate app based on app_path.
New files:
- tests/support_dirty_apps.py: CounterApp and EchoApp test apps
- tests/dirty/test_multi_app_routing.py: 13 routing tests covering
app loading, routing, state separation, error handling, and
concurrent requests
Replace lock-based request serialization with queue-based approach:
- Each worker now has a dedicated asyncio.Queue and consumer task
- route_request() submits (request, future) to queue and awaits future
- Consumer task processes requests sequentially per worker
- No lock contention - pure async queue operations
Benefits:
- Clearer separation of concerns
- Better visibility into request backlog (queue.qsize())
- Eliminates lock contention under high concurrency
Changes:
- worker_locks dict replaced with worker_queues and worker_consumers
- Added _start_worker_consumer() to create queue and consumer per worker
- Added _execute_on_worker() for actual worker communication
- Updated _cleanup_worker() to cancel consumer tasks
- Updated stop() to cancel all consumers before shutdown
Benchmark results (4 workers, isolated):
- throughput_10ms: 333 req/s, 0 failures
- overload_10ms (200 clients): 334 req/s, 0 failures
- All tests pass with perfect round-robin distribution
- Use dirty_threads config for thread pool size (default: 1)
- Enforce dirty_timeout at worker level via asyncio.wait_for
- Heartbeat runs independently, not blocked by task execution
- Document thread safety and state persistence in docstrings
Introduce Dirty Arbiters - a separate process pool for executing
long-running, blocking operations (AI model loading, heavy computation)
without blocking HTTP workers. Inspired by Erlang's dirty schedulers.
Key features:
- Completely separate from HTTP workers - can be killed/restarted independently
- Stateful - loaded resources persist in dirty worker memory
- Message-passing IPC via Unix sockets with JSON serialization
- Explicit execute() API from HTTP workers
- Asyncio-based for clean concurrent handling
Architecture:
- DirtyArbiter: manages the dirty worker pool, routes requests
- DirtyWorker: executes functions, maintains state, handles requests
- DirtyClient: sync/async API for HTTP workers to call dirty apps
- DirtyProtocol: length-prefixed JSON messages over Unix sockets
- DirtyApp: base class for dirty applications
Configuration options:
- dirty_apps: list of import paths for dirty applications
- dirty_workers: number of dirty workers (default: 0)
- dirty_timeout: task timeout in seconds (default: 300)
- dirty_graceful_timeout: shutdown timeout (default: 30)
Lifecycle hooks:
- on_dirty_starting(arbiter)
- dirty_post_fork(arbiter, worker)
- dirty_worker_init(worker)
- dirty_worker_exit(arbiter, worker)
Includes comprehensive test suite with 164 tests covering:
- Protocol encoding/decoding
- Worker and arbiter lifecycle
- Client sync/async APIs
- Signal handling
- Error handling and timeouts
- Integration tests
The CIDR network support added in 24.1.0 changed forwarded_allow_ips
and proxy_allow_ips from string lists to ipaddress.ip_network objects.
This broke external tools like uvicorn that expect strings.
This fix validates IP/CIDR format during config parsing but keeps the
string representation. Network objects are cached in Config methods
(forwarded_allow_networks() and proxy_allow_networks()) for efficient
IP checking without repeated conversions.
Also uses strict mode for ip_network validation to detect mistakes like
192.168.1.1/24 where host bits are set (should be 192.168.1.0/24).
Fixes#3458