Changelog¶
26.2.2 - 2026-09-06¶
Bug Fixes¶
- ASGI worker closed HTTP/1.1 connections without saying so: when the
worker would not reuse a connection (the client sent
Connection: close, keepalive is disabled, the worker is shutting down, or the response tripsmax_requests) it closed the socket without aConnection: closeheader, so a client treated the response as persistent and reused a socket the server had already closed. The header is now sent whenever the connection will close (#3726).
26.2.1 - 2026-09-05¶
Bug Fixes¶
-
ASGI worker dropped a pipelined request on keepalive: a second request sent on a kept-alive connection while the worker was finishing the first was left in the parser buffer, then discarded when the parser was reset between requests, so the connection hung until the client timed out. The buffered request is now recovered and served. Most visible with clients that pipeline or with frameworks that respond with
Transfer-Encoding: chunked. -
ASGI worker returned 502 for the uWSGI protocol: with
--worker-class asgi --protocol uwsgibehind nginxuwsgi_pass, every request failed withInvalid uWSGI header: incomplete header. The worker set up the HTTP/1 parser instead of a uWSGI reader, so inbound bytes never reached the uWSGI handler. The connection now starts a reader for the uWSGI protocol, builds the request body for the app, and setsraw_path. -
Chunked framing lines were read without a bound: a chunk-size line or trailer section that never terminates was reread in full on every socket read with no size limit, so one request could keep a worker busy without ever reaching the application. These lines are now bounded by the existing request limits and scanned incrementally, and a malformed chunked body answers
400 Bad Requestwith one log line instead of a traceback. -
HTTP/2 follow-up to the review: a bad request now resets its own stream instead of the connection; the request line and field limits and the method token rule apply to HTTP/2 requests; forbidden trailers are dropped;
Expect: 100-continueis answered;RST_STREAMfloods close the connection withENHANCE_YOUR_CALM; outbound frames respect the peer'sSETTINGS_MAX_FRAME_SIZE; a malformedHTTP2-Settingsheader refuses the h2c upgrade before any101goes out and an oversized upgrade body is not upgraded;http2_initial_window_sizeandhttp2_max_concurrent_streamsare bounded at startup,http2_max_header_list_size = 0means unlimited as documented,h3is no longer accepted inhttp_protocols, andh2without the h2 package is a configuration error. Ongthreadandgeventthe h2c-upgraded stream is cleaned up like any other, requests queued behind a body read are served before themax_requestsGOAWAY, which names the last stream served, peer protocol errors are logged at debug, and responses carryServerandDate. On the ASGI workersend()raises anOSErroronce the peer is gone (ASGI 2.4), trailers announced onhttp.response.startare sent after the body, no-body statuses drop their framing headers, the body wait is bounded bytimeout, write backpressure reacheswriter.drain(), and an idle connection closes afterkeepalive. h2spec runs against every HTTP/2 worker in the docker suites. -
HTTP/2 request bodies were buffered in full before dispatch: DATA frames were kept in two buffers, copied twice more when the request was built, and flow control credit went back to the peer as each frame arrived, so a client that never sent END_STREAM could grow a worker without the application being called. A request is now dispatched on its headers and
wsgi.input(or ASGIreceive()) pulls the body from the stream as it arrives, returning window credit only for what the application has read. A peer can have no more than the receive window in flight per stream. A body the application leaves unread is cut off withRST_STREAM(NO_ERROR)once the response is sent. On the ASGI worker, streams on a connection are served concurrently. Only listeners withh2inhttp_protocolswere affected. -
HTTP/2 review fixes: a review of the HTTP/2 support found these, all fixed on every h2-capable worker unless noted. A graceful
GOAWAYfrom the peer is honoured wherever it lands in a read: established streams finish, later ones are refused, and no private h2 state is touched. Waiting for send credit is bounded bytimeoutinstead of a fixed five seconds; a peer that stops reading getsRST_STREAM(CANCEL)and the application is stopped rather than told the response succeeded. An application error after the response headers went out resets the stream instead of sending a secondHEADERSblock, which corrupted the HPACK table for every later response. An empty final body chunk now ends the stream. Ongthreadandgevent, frames read while waiting for credit are handled in order, an idle connection is closed afterkeepaliveand a body that stalls fortimeoutis cancelled, so a peer can no longer pin a thread, and streams the peer resets before they are served are dropped soHEADERS+RST_STREAMfloods cannot grow the worker. On the ASGI worker,receive()after the body blocks until the peer goes away instead of spinning the event loop, a reader paused under backpressure is resumed, and in-flight streams get the disconnect grace period before they are cancelled. -
Worker timeout used to dump a 500 response onto a body that had already started, because
sys.exit()from the abort handler is aBaseException. Once headers are out we just close the connection (#3410).
26.2.0 - 2026-08-24¶
New Features¶
- Cleartext HTTP/2 (h2c):
http2_cleartextacceptsprior-knowledge,upgrade,bothoroff(the default). Withprior-knowledge, a connection opening with the HTTP/2 preface is served as HTTP/2, which is what a TLS-terminating proxy in front of gunicorn needs to avoid dropping to HTTP/1.1 upstream.upgradehonours an HTTP/1.1Upgrade: h2crequest. Both work on the gthread, gevent and ASGI workers. Only peers inforwarded_allow_ipsare considered; anything else from such a peer is refused with 400 rather than silently downgraded. Each mechanism is enabled separately, so turning one on does not turn the other on (#3489, #3663, #3711).
Security¶
- HTTP/2 bypassed the header policy:
HTTP2Requestbuilt its headers straight from the stream, so nothing the HTTP/1 path enforces applied over HTTP/2: the underscore andheader_mappolicy, duplicateHostandContent-Type, control characters in values, and theforwarded_allow_ipstrust gate. An untrusted client could setSCRIPT_NAMEand forgeHTTP_*entries in the WSGI environ, and decidewsgi.url_schemethrough:scheme. The policy now lives on a mixin both request classes share, and the scheme is derived from the transport (#3705).
Bug Fixes¶
-
WSGI HTTP/2 responses were buffered whole: both WSGI workers collected the entire body in memory before sending anything. They write through an
HTTP2Responsenow, so the body leaves as it is produced (#3709). -
No-body responses carried a body over HTTP/2: HEAD, 204 and 304 sent application body bytes on all three workers, while the HTTP/1 path had always dropped them per RFC 9110. They no longer do (#3709, #3710).
-
Events lost during flow-control waits: while blocked waiting for a WINDOW_UPDATE, both HTTP/2 connections read from the socket and discarded every event that was not a stream reset or connection termination, losing requests and body data outright. They are queued for the main loop now (#3709, #3710).
-
sendfile()on HTTP/2: it was guarded bycfg.is_ssl, which covered HTTP/2 only for as long as HTTP/2 implied TLS. It is refused on HTTP/2 responses directly, so cleartext cannot bypass HTTP/2 framing (#3709). -
Request bodies dropped on
Upgraderequests: on the ASGI worker with the fast parser, any request carrying anUpgradeheader reached the application with an empty body, whatever the header's value and with HTTP/2 switched off. The parser treated the header as meaning the rest of the connection was no longer HTTP/1, so it never read the body it had just been told the length of. Fixed ingunicorn_h1c0.6.9, which the requirement below now pins (#3711).
Changes¶
- Fast HTTP Parser: require
gunicorn_h1c >= 0.6.9, which addsremaining()for recovering bytes pipelined behind a completed message, and stops anUpgradeheader from suppressing body parsing (#3711).
Documentation¶
-
Describe
http_parserin its own terms rather than as an ASGI-only setting; it applies to the WSGI workers too. Thanks to @methane (#3704). -
Correct the default control socket path in the
gunicorn.cguide. Thanks to @cormier (#3703). -
State the supported Python version as 3.10+ in the README, matching
requires-python. Thanks to @Rotzbua (#3712). -
Point CONTRIBUTING at the mkdocs settings reference instead of the retired Sphinx path. Thanks to @melbinjp (#3690).
-
Fix the sponsor logo path on the sponsor page (#3700).
26.1.0 - 2026-08-18¶
New Features¶
- Glob patterns in
reload_extra_files: entries containing*,?or[are treated as patterns, soui/*/config.jsonwatches every view's config without listing them one by one. Patterns are re-expanded on every reload check rather than once at startup, so a file created later starts being watched without restarting gunicorn, and**recurses. A pattern matching nothing warns instead of failing, since with live expansion it may match later (#1643, #3662).
Security¶
- Dependency floors raised past known advisories: every declared floor was
checked against the advisory database.
tornado,h2,setuptoolsandpymdown-extensionspermitted vulnerable versions and now require the first clean release;pytestandhttpxwere unpinned and now carry floors. Thetornadoexample pinnedtornado<6, which was both the source of several advisories and older than the>=6.5.0the tornado worker needs, so the example could not run as pinned.
Bug Fixes¶
-
SIGHUP did not reload the logger configuration:
Arbiter.reload()re-read the configuration file but kept using the logger built at startup, calling onlyreopen_files()on its existing handlers. Changes tologconfig,logconfig_dict,logconfig_jsonandloglevelwere ignored until a full restart, which in containers meant replacing the pod. The existing logger now re-runs its setup on reload, so new handlers, formats and levels take effect while the process identity and its listeners are preserved, and re-running the setup no longer stacks duplicate syslog handlers. An invalid log configuration on reload is not fatal either: the error is reported on stderr, the previous working configuration is restored and the master keeps running with it (#3353). -
Truncated chunked bodies accepted: RFC 9112 section 7.1.2 ends a chunked body with
0 CRLF CRLF, the second CRLF being the mandatory empty trailer section.ChunkedReader.parse_chunk_size()swallowed theNoMoreDataraised while scanning for it, so a body cut short right after the last chunk line was treated as complete instead of rejected. It now raisesChunkMissingTerminator(#3382, #3685). -
--spewcrashed on dynamically generated code: the trace hook indexed the 2-tuple returned byinspect.getsourcelines()by line number rather than indexing the list of lines, so a frame with no__file__raisedAttributeError: 'int' object has no attribute 'rstrip'on line 1 andIndexErrorbeyond it. The tuple is now unpacked and offset by the source's starting line (#3344, #3495). -
Duplicate
HostandContent-Typeheaders accepted: RFC 9110 section 5.3 allows only one of each, and a repeat cannot be merged into a list, so the message means different things to gunicorn and to anything downstream. Both are now rejected withInvalidHeader. The check lives in the policy hook shared by both parsers, so the pure-Python and fast parsers agree. DuplicateContent-Lengthwas already rejected and is unchanged (#3366, #3548). -
Non-worker children reported as failed workers:
reap_workers()reaps every child throughwaitpid(-1), including processes the kernel reparented onto gunicorn when it runs as PID 1 in a container, but it logged the exit status before checking whether the pid was ever a worker. An unrelated process producedWorker (pid:N) exited with code Mand triggered alerts. More seriously, such a process exiting with code 3 or 4 raisedHaltServerand shut the server down. Ownership is now established first: the dirty arbiter is reported as itself, unknown children are reaped silently at debug level, and only real workers can halt the server (#3220, #3566). -
Dirty arbiter exits were invisible on SIGCHLD:
handle_chld()calledreap_workers()first, whosewaitpid(-1)claimed the dirty arbiter beforereap_dirty_arbiter()could identify it, so the latter always hitECHILDand its reporting never ran. The dirty arbiter is now reaped first, andreap_workers()recognises it if it exits mid-loop. -
Dirty arbiter returned stale responses after a worker timeout: when a request reached
dirty_timeoutthe arbiter answered the client with a timeout error but kept the worker connection open. The worker's late response was then the first message waiting on that socket, so the next request routed to the same worker received the previous request's result, and every request after it stayed one response behind. The connection is now closed on timeout, so the late answer is discarded with it (#3626). -
ASGI connection count leaked on server-initiated close:
nr_connswas only decremented inconnection_lost(), behind a guard keyed on the same flag_close_transport()sets first. Every close the server started (aConnection: closeresponse, a keepalive timeout, an error abort) leaked one count, soASGIWorker._shutdown()ran the fullgraceful_timeoutand warned about connections that were already gone. The guard now uses its own flag, so the decrement and the rest of the cleanup run exactly once whichever side closes first (#3661). -
Inotify reloader on cwd-relative extra files:
reload_extra_filesentries with no directory part (for example.env) produced an empty dirname, and watching it raisedInotifyErrorwithENOENT. The current directory is now watched as.(#3377, #3667). -
StatsD zero-valued metrics: gauges, counters, histograms and timers reporting
0were silently dropped because the value was tested for truthiness. OnlyNoneis skipped now (#3676). -
Spurious no-body warning from
sendfile(): a HEAD, 204 or 304 response served throughsendfile()warned about dropped body bytes even when the file was empty and nothing was dropped. It now warns only when there are bytes to drop, matchingwrite()(#3684). -
Bare
exceptin the gevent websocket example: narrowed toexcept Exception(#3683). -
ASGI
receive()cancellation: Letasyncio.CancelledErrorpropagate fromBodyReceiverinstead of swallowing it and returninghttp.disconnect. Frameworks that cancel their disconnect listener after the response completes (Django) no longer see the cancel masked, sorequest_finishedfires andclose_old_connections()runs. Fixes idle database connections leaking since 25.1.0 (#3627, #3654). -
Control socket leak on SIGHUP reload: The control thread is now marked ready once its loop and server are live, and the stop paths wait on that readiness before scheduling shutdown. Reloads no longer leak one thread and its selector fd plus unix socket per worker, which eventually raised "too many open files" (#3648).
-
WSGI body framing on HEAD/1xx/204/304: Mirror the ASGI strip-and-warn behavior on the WSGI path.
Content-Lengthis stripped on 1xx/204 per RFC 9110 section 6.4.2, body bytes are dropped for no-body responses in bothwrite()andsendfile(), and a single warning is logged per request (#3413).
Refactoring¶
- Pass log arguments to the logger instead of pre-formatting the worker
termination message in
Arbiter.reap_workers()(#3678).
Changes¶
-
packagingis no longer a runtime dependency: it was only ever imported by the gevent worker, to compare gevent's version. It moved to thegeventandtestingextras, so a plainpip install gunicornpulls in nothing (#3643). -
Fast HTTP Parser: Require
gunicorn_h1c >= 0.6.6, which rejects duplicateHostandContent-Typeheaders in the C parser itself. Gunicorn already refuses them on both the WSGI and ASGI paths, so this changes nothing that is reachable; it moves the rejection to where the bytes are read and lets the ASGI corpus exercise those cases against the fast parser directly.
26.0.0 - 2026-05-05¶
Breaking Changes¶
- Eventlet worker removed: The
eventletworker class has been dropped. Migrate togevent,gthread, ortornado.
New Features¶
-
ASGI Framework Compatibility Suite: New end-to-end compatibility test harness covering Starlette, FastAPI, Litestar, Quart, Sanic, and BlackSheep. Current grid passes 438/444 tests (98%).
-
ASGI Test Suite Expansion: 134 additional ASGI unit tests covering protocol semantics, lifespan, websockets, and chunked framing.
Security¶
- HTTP/1.1 Request-Target Validation (RFC 9112 sections 3.2.3, 3.2.4):
- Reject
authority-formrequest-target outsideCONNECT - Reject
asterisk-formrequest-target outsideOPTIONS -
Reject
relative-referencerequest-targets -
Header Field Hardening (RFC 9110):
- Reject control characters in header field-value (section 5.5)
- Reject forbidden trailer field-names (section 6.5.1)
-
Reject
Content-Lengthlist form (RFC 9112 section 6.3) -
Request Smuggling Hardening:
- Tighten keepalive gate and scope
finish_bodybyte cap - Keep
_body_receiveralive across the keepalive smuggling gate so pipelined requests cannot re-enter a closed body -
Address parser/protocol findings from a six-point WSGI/ASGI audit
-
PROXY Protocol (ASGI): Enforce
proxy_allow_ipsand tighten v1/v2 parsing in the ASGI callback parser. -
Connection Draining: Drain the connection on close per RFC 9112 section 9.6 to prevent reset-on-close truncation.
Bug Fixes¶
- Body Framing on HEAD/204/304:
- Keep
Content-Lengthon HEAD and 304 responses (#3621) - Drop body framing on HEAD/204/304 even when the framework set it
-
Warn once when an ASGI app emits a body for a no-body response
-
HTTP/2 ASGI:
- Fix
_handle_stream_endedto set_body_completein the async HTTP/2 handler so request bodies finalize correctly on stream end -
Add
InvalidChunkExtensionmapping and fast-parser support in ASGI tests (#3565) -
HTTP/1.1 100-Continue: Stop adding
Transfer-Encoding: chunkedto 100-Continue interim responses. -
WebSocket Close Handshake (RFC 6455):
- Comply with the close handshake state machine
- Close the transport after the close handshake completes
-
Fix binary send when the
textkey isNone -
Early Hints: Validate headers in the
early_hintscallback to matchprocess_headers; pass only the header name toInvalidHeader(#3588). -
ASGI Framework Fixes:
- Fix ASGI disconnect handling for Django-style apps
- Fix Litestar request handling (use raw ASGI receive for body/headers)
- Fix Litestar HTTP endpoints for compatibility tests
- Fix Quart headers endpoint to normalize keys to lowercase
- Fix Quart WebSocket close test app (missing
accept()) - Fix duplicate
Transfer-Encodingheader for BlackSheep streaming
Refactoring¶
- Split
BodyReceiver._closedinto separate transport and body-wait flags for clearer keepalive/EOF semantics.
Changes¶
-
Fast HTTP Parser: Require
gunicorn_h1c >= 0.6.5. Drop the lastpython_onlytest markers; the C extension is now used wherever available (CPython only; PyPy continues to use the Python parser). -
Test Dependencies: Add
h2anduvloopto thetestingextra; removeeventlet. -
Docker Build: Bump GitHub Actions
docker/setup-qemu-action,docker/setup-buildx-action,docker/login-action,docker/build-push-action, anddocker/metadata-actionto current major versions.
25.3.0 - 2026-03-26¶
Bug Fixes¶
-
HTTP/2 ASGI Body Duplication: Fix request body being received twice in HTTP/2 ASGI requests, causing JSON parsing errors with "Extra data" messages (#3558)
-
ASGI Chunked EOF Handling: Add
finish()method to callback parser to handle chunked encoding edge case where connection closes before final CRLF after zero-chunk -
HTTP/2 Documentation: Fix
http_protocolsexamples to use comma-separated string instead of list syntax (#3561) -
Chunked Encoding: Reject chunk extensions containing bare CR bytes per RFC 9112 (#3556)
-
Request Line Limit: Fix
--limit-request-line 0to mean unlimited as documented, instead of using default maximum. Works with both Python and fast C parser. (#3563)
Security¶
- ASGI Parser Header Validation: Add security checks per RFC 9110/9112:
- Reject duplicate Content-Length headers
- Reject requests with both Content-Length and Transfer-Encoding
- Reject chunked transfer encoding in HTTP/1.0
- Reject stacked chunked encoding
- Validate Transfer-Encoding values
- Strict chunk size validation
Changes¶
-
Fast HTTP Parser: Update to gunicorn_h1c >= 0.6.3 for
asgi_headersproperty andInvalidChunkExtensionvalidation for bare CR rejection -
ASGI PROXY Protocol: Add PROXY protocol v1/v2 support to callback parser
-
Docker Images: Update to Python 3.14
25.2.0 - 2026-03-24¶
New Features¶
- Fast HTTP Parser (gunicorn_h1c 0.6.0): Integrate new exception types and limit parameters from gunicorn_h1c 0.6.0 for both WSGI and ASGI workers
- Requires gunicorn_h1c >= 0.6.0 for
http_parser='fast' - Falls back to Python parser in
automode if version not met - Proper HTTP status codes for limit errors (414, 431)
Bug Fixes¶
-
uWSGI Async Workers: Fix
InvalidUWSGIHeader: incomplete headererror when using gevent or gthread workers with uwsgi protocol behind nginx. (#3552, PR #3554) -
FileWrapper Iterator Protocol: Add
__iter__and__next__methods toFileWrapperfor full PEP 3333 compliance. Previously only supported old-style__getitem__iteration which broke code explicitly usingiter()ornext(). (#3396, PR #3550)
Performance¶
- ASGI HTTP Parser Optimizations: Improve ASGI worker HTTP parsing performance
- Callback-based parsing with direct
bytearraybuffer operations - Use
bytearray.find()directly instead of converting to bytes first - Use index-based iteration for header parsing instead of
list.pop(0)(O(1) vs O(n))
25.1.0 - 2026-02-13¶
New Features¶
- Control Interface (gunicornc): Add interactive control interface for managing running Gunicorn instances, similar to birdc for BIRD routing daemon (PR #3505)
- Unix socket-based communication with JSON protocol
- Interactive mode with readline support and command history
- Commands:
show all/workers/dirty/config/stats/listeners - Worker management:
worker add/remove/kill,dirty add/remove - Server control:
reload,reopen,shutdown - New settings:
--control-socket,--control-socket-mode,--no-control-socket - New CLI tool:
gunicorncfor connecting to control socket -
See Control Interface Guide for details
-
Dirty Stash: Add global shared state between workers via
dirty.stash(PR #3503) - In-memory key-value store accessible by all workers
- Supports get, set, delete, clear, keys, and has operations
-
Useful for sharing state like feature flags, rate limits, or cached data
-
Dirty Binary Protocol: Implement efficient binary protocol for dirty arbiter IPC using TLV (Type-Length-Value) encoding (PR #3500)
- More efficient than JSON for binary data
- Supports all Python types: str, bytes, int, float, bool, None, list, dict
-
Better performance for large payloads
-
Dirty TTIN/TTOU Signals: Add dynamic worker scaling for dirty arbiters (PR #3504)
- Send SIGTTIN to increase dirty workers
- Send SIGTTOU to decrease dirty workers
- Respects minimum worker constraints from app configurations
Changes¶
- ASGI Worker: Promoted from beta to stable
- Dirty Arbiters: Now marked as beta feature
Documentation¶
- Fix Markdown formatting in /configure documentation
25.0.3 - 2026-02-07¶
Bug Fixes¶
-
Fix RuntimeError when StopIteration is raised inside ASGI response body coroutine (PEP 479 compliance)
-
Fix deprecation warning for passing maxsplit as positional argument in
re.split()(Python 3.13+)
25.0.2 - 2026-02-06¶
Bug Fixes¶
-
Fix ASGI concurrent request failures through nginx proxy by normalizing sockaddr tuples to handle both 2-tuple (IPv4) and 4-tuple (IPv6) formats (PR #3485)
-
Fix graceful disconnect handling for ASGI worker to properly handle client disconnects without raising exceptions (PR #3485)
-
Fix lazy import of dirty module for gevent compatibility - prevents import errors when concurrent.futures is imported before gevent monkey-patching (PR #3483)
Changes¶
-
Refactor: Extract
_normalize_sockaddrutility function for consistent socket address handling across workers -
Add license headers to all Python source files
-
Update copyright year to 2026 in LICENSE and NOTICE files
25.0.1 - 2026-02-02¶
Bug Fixes¶
- Fix ASGI streaming responses (SSE) hanging: add chunked transfer encoding for HTTP/1.1 responses without Content-Length header. Without chunked encoding, clients wait for connection close to determine end-of-response.
Changes¶
- Update celery_alternative example to use FastAPI with native ASGI worker and uvloop for async task execution
Testing¶
- Add ASGI compliance test suite with Docker-based integration tests covering HTTP, WebSocket, streaming, lifespan, framework integration (Starlette, FastAPI), HTTP/2, and concurrency scenarios
25.0.0 - 2026-02-01¶
New Features¶
- Dirty Arbiters: Separate process pool for executing long-running, blocking operations (AI model loading, heavy computation) without blocking HTTP workers (PR #3460)
- Inspired by Erlang's dirty schedulers
- Asyncio-based with Unix socket IPC
- Stateful workers that persist loaded resources
- New settings:
--dirty-app,--dirty-workers,--dirty-timeout,--dirty-threads,--dirty-graceful-timeout -
Lifecycle hooks:
on_dirty_starting,dirty_post_fork,dirty_worker_init,dirty_worker_exit -
Per-App Worker Allocation for Dirty Arbiters: Control how many dirty workers load each app for memory optimization with heavy models (PR #3473)
- Set
workersclass attribute on DirtyApp (e.g.,workers = 2) - Or use config format
module:class:N(e.g.,myapp:HeavyModel:2) - Requests automatically routed to workers with the target app
- New exception
DirtyNoWorkersAvailableErrorfor graceful error handling -
Example: 8 workers × 10GB model = 80GB → with
workers=2: 20GB (75% savings) -
HTTP/2 Support (Beta): Native HTTP/2 (RFC 7540) support for improved performance with modern clients (PR #3468)
- Multiplexed streams over a single connection
- Header compression (HPACK)
- Flow control and stream prioritization
- Works with gthread, gevent, and ASGI workers
- New settings:
--http-protocols,--http2-max-concurrent-streams,--http2-initial-window-size,--http2-max-frame-size,--http2-max-header-list-size - Requires SSL/TLS and h2 library:
pip install gunicorn[http2] - See HTTP/2 Guide for details
-
New example:
examples/http2_gevent/with Docker and tests -
HTTP 103 Early Hints: Support for RFC 8297 Early Hints to enable browsers to preload resources before the final response (PR #3468)
- WSGI:
environ['wsgi.early_hints'](headers)callback - ASGI:
http.response.informationalmessage type -
Works with both HTTP/1.1 and HTTP/2
-
uWSGI Protocol for ASGI Worker: The ASGI worker now supports receiving requests via the uWSGI binary protocol from nginx (PR #3467)
Bug Fixes¶
-
Fix HTTP/2 ALPN negotiation for the gevent worker when
do_handshake_on_connectis False (the default). The TLS handshake is now explicitly performed before checkingselected_alpn_protocol(). -
Fix setproctitle initialization with systemd socket activation (#3465)
-
Fix
Expect: 100-continuehandling: ignore the header for HTTP/1.0 requests since 100-continue is only valid for HTTP/1.1+ (PR #3463) -
Fix missing
_expected_100_continueattribute in UWSGIRequest -
Disable setproctitle on macOS to prevent segfaults during process title updates
-
Publish full exception traceback when the application fails to load (#3462)
-
Fix ASGI: quick shutdown on SIGINT/SIGQUIT, graceful on SIGTERM
Removals¶
- Eventlet Worker: The
eventletworker has been removed. Eventlet itself is no longer actively maintained; the worker was deprecated in 25.x and is now gone. Migrate togevent,gthread, or one of the ASGI workers.
Changes¶
- Remove obsolete Makefile targets (PR #3471)
24.1.1 - 2026-01-24¶
Bug Fixes¶
- Fix
forwarded_allow_ipsandproxy_allow_ipsto remain as strings for backward compatibility with external tools like uvicorn. Network validation now uses strict mode to detect invalid CIDR notation (e.g.,192.168.1.1/24where host bits are set) (#3458, PR #3459)
24.1.0 - 2026-01-23¶
New Features¶
- Official Docker Image: Gunicorn now publishes official Docker images to GitHub
Container Registry at
ghcr.io/benoitc/gunicorn - Based on Python 3.12 slim image
- Uses recommended worker formula (2 × CPU + 1)
-
Configurable via environment variables
-
PROXY Protocol v2 Support: Extended PROXY protocol implementation to support the binary v2 format in addition to the existing text-based v1 format
- New
--proxy-protocolmodes:off,v1,v2,auto -
Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources
-
CIDR Network Support:
--forwarded-allow-ipsand--proxy-allow-fromnow accept CIDR notation (e.g.,192.168.0.0/16) for specifying trusted networks -
Socket Backlog Metric: New
gunicorn.socket.backloggauge metric reports the current socket backlog size on Linux systems -
InotifyReloader Enhancement: The inotify-based reloader now watches newly imported modules, not just those loaded at startup
Bug Fixes¶
- Fix signal handling regression where SIGCLD alias caused errors on Linux
- Fix socket blocking mode on keepalive connections with async workers
- Handle
SSLWantReadErrorinfinish_body()to prevent worker hangs - Log SIGTERM as info level instead of warning
- Print exception details to stderr when worker fails to boot
- Fix
unreader.unread()to prepend data to buffer instead of appending - Prevent
RecursionErrorwhen pickling Config objects
24.0.0 - 2026-01-23¶
New Features¶
- ASGI Worker (Beta): Native asyncio-based ASGI support for running async Python frameworks like FastAPI, Starlette, and Quart without external dependencies
- HTTP/1.1 with keepalive connections
- WebSocket support
- Lifespan protocol for startup/shutdown hooks
-
Optional uvloop for improved performance
-
uWSGI Binary Protocol: Support for receiving requests from nginx via
uwsgi_passdirective -
Documentation Migration: Migrated to MkDocs with Material theme
Security¶
- gevent: Require gevent >= 24.10.1 (CVE-2023-41419, CVE-2024-3219)
- tornado: Require tornado >= 6.5.0 (CVE-2025-47287)
23.0.0 - 2024-08-10¶
- minor docs fixes (PR #3217, PR #3089, PR #3167)
- worker_class parameter accepts a class (PR #3079)
- fix deadlock if request terminated during chunked parsing (PR #2688)
- permit receiving Transfer-Encodings: compress, deflate, gzip (PR #3261)
- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still (PR #3261)
- sdist generation now explicitly excludes sphinx build folder (PR #3257)
- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising
TypeError(PR #2336) - raise correct Exception when encounting invalid chunked requests (PR #3258)
- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore (PR #3192)
- include IPv6 loopback address
[::1]in default for forwarded-allow-ips and proxy-allow-ips (PR #3192)
Note
- The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release
- Review your forwarded-allow-ips setting if you are still not seeing the SCRIPT_NAME transmitted
- Review your forwarder-headers setting if you are missing headers after upgrading from a version prior to 22.0.0
Breaking changes¶
- refuse requests where the uri field is empty (PR #3255)
- refuse requests with invalid CR/LR/NUL in heade field values (PR #3253)
- remove temporary
--tolerate-dangerous-framingswitch from 22.0 (PR #3260) - If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies.
22.0.0 - 2024-04-17¶
- use
utimeto notify workers liveness - migrate setup to pyproject.toml
- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors)
- parsing additional requests is no longer attempted past unsupported request framing
- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits)
- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error
- Trailer fields are no longer inspected for headers indicating secure scheme
- support Python 3.12
Breaking changes¶
- minimum version is Python 3.7
- the limitations on valid characters in the HTTP method have been bounded to Internet Standards
- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md)
- HTTP methods are no longer casefolded by default (IANA method registry contains none affected)
- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md)
- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported)
- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted
- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software
- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits)
- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling)
- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies)
Security¶
- fix CVE-2024-1135