Skip to content

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 trips max_requests) it closed the socket without a Connection: close header, 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 uwsgi behind nginx uwsgi_pass, every request failed with Invalid 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 sets raw_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 Request with 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-continue is answered; RST_STREAM floods close the connection with ENHANCE_YOUR_CALM; outbound frames respect the peer's SETTINGS_MAX_FRAME_SIZE; a malformed HTTP2-Settings header refuses the h2c upgrade before any 101 goes out and an oversized upgrade body is not upgraded; http2_initial_window_size and http2_max_concurrent_streams are bounded at startup, http2_max_header_list_size = 0 means unlimited as documented, h3 is no longer accepted in http_protocols, and h2 without the h2 package is a configuration error. On gthread and gevent the h2c-upgraded stream is cleaned up like any other, requests queued behind a body read are served before the max_requests GOAWAY, which names the last stream served, peer protocol errors are logged at debug, and responses carry Server and Date. On the ASGI worker send() raises an OSError once the peer is gone (ASGI 2.4), trailers announced on http.response.start are sent after the body, no-body statuses drop their framing headers, the body wait is bounded by timeout, write backpressure reaches writer.drain(), and an idle connection closes after keepalive. 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 ASGI receive()) 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 with RST_STREAM(NO_ERROR) once the response is sent. On the ASGI worker, streams on a connection are served concurrently. Only listeners with h2 in http_protocols were 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 GOAWAY from 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 by timeout instead of a fixed five seconds; a peer that stops reading gets RST_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 second HEADERS block, which corrupted the HPACK table for every later response. An empty final body chunk now ends the stream. On gthread and gevent, frames read while waiting for credit are handled in order, an idle connection is closed after keepalive and a body that stalls for timeout is cancelled, so a peer can no longer pin a thread, and streams the peer resets before they are served are dropped so HEADERS+RST_STREAM floods 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 a BaseException. Once headers are out we just close the connection (#3410).

26.2.0 - 2026-08-24

New Features

  • Cleartext HTTP/2 (h2c): http2_cleartext accepts prior-knowledge, upgrade, both or off (the default). With prior-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. upgrade honours an HTTP/1.1 Upgrade: h2c request. Both work on the gthread, gevent and ASGI workers. Only peers in forwarded_allow_ips are 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: HTTP2Request built its headers straight from the stream, so nothing the HTTP/1 path enforces applied over HTTP/2: the underscore and header_map policy, duplicate Host and Content-Type, control characters in values, and the forwarded_allow_ips trust gate. An untrusted client could set SCRIPT_NAME and forge HTTP_* entries in the WSGI environ, and decide wsgi.url_scheme through :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 HTTP2Response now, 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 by cfg.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 Upgrade requests: on the ASGI worker with the fast parser, any request carrying an Upgrade header 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 in gunicorn_h1c 0.6.9, which the requirement below now pins (#3711).

Changes

  • Fast HTTP Parser: require gunicorn_h1c >= 0.6.9, which adds remaining() for recovering bytes pipelined behind a completed message, and stops an Upgrade header from suppressing body parsing (#3711).

Documentation

  • Describe http_parser in 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.c guide. 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, so ui/*/config.json watches 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, setuptools and pymdown-extensions permitted vulnerable versions and now require the first clean release; pytest and httpx were unpinned and now carry floors. The tornado example pinned tornado<6, which was both the source of several advisories and older than the >=6.5.0 the 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 only reopen_files() on its existing handlers. Changes to logconfig, logconfig_dict, logconfig_json and loglevel were 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 the NoMoreData raised while scanning for it, so a body cut short right after the last chunk line was treated as complete instead of rejected. It now raises ChunkMissingTerminator (#3382, #3685).

  • --spew crashed on dynamically generated code: the trace hook indexed the 2-tuple returned by inspect.getsourcelines() by line number rather than indexing the list of lines, so a frame with no __file__ raised AttributeError: 'int' object has no attribute 'rstrip' on line 1 and IndexError beyond it. The tuple is now unpacked and offset by the source's starting line (#3344, #3495).

  • Duplicate Host and Content-Type headers 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 with InvalidHeader. The check lives in the policy hook shared by both parsers, so the pure-Python and fast parsers agree. Duplicate Content-Length was already rejected and is unchanged (#3366, #3548).

  • Non-worker children reported as failed workers: reap_workers() reaps every child through waitpid(-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 produced Worker (pid:N) exited with code M and triggered alerts. More seriously, such a process exiting with code 3 or 4 raised HaltServer and 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() called reap_workers() first, whose waitpid(-1) claimed the dirty arbiter before reap_dirty_arbiter() could identify it, so the latter always hit ECHILD and its reporting never ran. The dirty arbiter is now reaped first, and reap_workers() recognises it if it exits mid-loop.

  • Dirty arbiter returned stale responses after a worker timeout: when a request reached dirty_timeout the 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_conns was only decremented in connection_lost(), behind a guard keyed on the same flag _close_transport() sets first. Every close the server started (a Connection: close response, a keepalive timeout, an error abort) leaked one count, so ASGIWorker._shutdown() ran the full graceful_timeout and 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_files entries with no directory part (for example .env) produced an empty dirname, and watching it raised InotifyError with ENOENT. The current directory is now watched as . (#3377, #3667).

  • StatsD zero-valued metrics: gauges, counters, histograms and timers reporting 0 were silently dropped because the value was tested for truthiness. Only None is skipped now (#3676).

  • Spurious no-body warning from sendfile(): a HEAD, 204 or 304 response served through sendfile() 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, matching write() (#3684).

  • Bare except in the gevent websocket example: narrowed to except Exception (#3683).

  • ASGI receive() cancellation: Let asyncio.CancelledError propagate from BodyReceiver instead of swallowing it and returning http.disconnect. Frameworks that cancel their disconnect listener after the response completes (Django) no longer see the cancel masked, so request_finished fires and close_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-Length is stripped on 1xx/204 per RFC 9110 section 6.4.2, body bytes are dropped for no-body responses in both write() and sendfile(), 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

  • packaging is no longer a runtime dependency: it was only ever imported by the gevent worker, to compare gevent's version. It moved to the gevent and testing extras, so a plain pip install gunicorn pulls in nothing (#3643).

  • Fast HTTP Parser: Require gunicorn_h1c >= 0.6.6, which rejects duplicate Host and Content-Type headers 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 eventlet worker class has been dropped. Migrate to gevent, gthread, or tornado.

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-form request-target outside CONNECT
  • Reject asterisk-form request-target outside OPTIONS
  • Reject relative-reference request-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-Length list form (RFC 9112 section 6.3)

  • Request Smuggling Hardening:

  • Tighten keepalive gate and scope finish_body byte cap
  • Keep _body_receiver alive 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_ips and 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-Length on 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_ended to set _body_complete in the async HTTP/2 handler so request bodies finalize correctly on stream end
  • Add InvalidChunkExtension mapping and fast-parser support in ASGI tests (#3565)

  • HTTP/1.1 100-Continue: Stop adding Transfer-Encoding: chunked to 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 text key is None

  • Early Hints: Validate headers in the early_hints callback to match process_headers; pass only the header name to InvalidHeader (#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-Encoding header for BlackSheep streaming

Refactoring

  • Split BodyReceiver._closed into separate transport and body-wait flags for clearer keepalive/EOF semantics.

Changes

  • Fast HTTP Parser: Require gunicorn_h1c >= 0.6.5. Drop the last python_only test markers; the C extension is now used wherever available (CPython only; PyPy continues to use the Python parser).

  • Test Dependencies: Add h2 and uvloop to the testing extra; remove eventlet.

  • Docker Build: Bump GitHub Actions docker/setup-qemu-action, docker/setup-buildx-action, docker/login-action, docker/build-push-action, and docker/metadata-action to 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_protocols examples 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 0 to 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_headers property and InvalidChunkExtension validation 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 auto mode if version not met
  • Proper HTTP status codes for limit errors (414, 431)

Bug Fixes

  • uWSGI Async Workers: Fix InvalidUWSGIHeader: incomplete header error when using gevent or gthread workers with uwsgi protocol behind nginx. (#3552, PR #3554)

  • FileWrapper Iterator Protocol: Add __iter__ and __next__ methods to FileWrapper for full PEP 3333 compliance. Previously only supported old-style __getitem__ iteration which broke code explicitly using iter() or next(). (#3396, PR #3550)

Performance

  • ASGI HTTP Parser Optimizations: Improve ASGI worker HTTP parsing performance
  • Callback-based parsing with direct bytearray buffer 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: gunicornc for 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_sockaddr utility 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 workers class 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 DirtyNoWorkersAvailableError for 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.informational message 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_connect is False (the default). The TLS handshake is now explicitly performed before checking selected_alpn_protocol().

  • Fix setproctitle initialization with systemd socket activation (#3465)

  • Fix Expect: 100-continue handling: ignore the header for HTTP/1.0 requests since 100-continue is only valid for HTTP/1.1+ (PR #3463)

  • Fix missing _expected_100_continue attribute 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 eventlet worker has been removed. Eventlet itself is no longer actively maintained; the worker was deprecated in 25.x and is now gone. Migrate to gevent, 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_ips and proxy_allow_ips to 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/24 where 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-protocol modes: off, v1, v2, auto
  • Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources

  • CIDR Network Support: --forwarded-allow-ips and --proxy-allow-from now accept CIDR notation (e.g., 192.168.0.0/16) for specifying trusted networks

  • Socket Backlog Metric: New gunicorn.socket.backlog gauge 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 SSLWantReadError in finish_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 RecursionError when 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_pass directive

  • 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-framing switch 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 utime to 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

History