Skip to content

[Triage] Switching from Gunicorn 23 to 25 breaks Django + k8s workload #3518

@benoitc

Description

@benoitc

There's a significant behavioral change in the gthread worker between v23 and v24+/v25:

v23 behavior (gunicorn/workers/gthread.py):

  def accept(self, server, listener):
      sock, client = listener.accept()
      conn = TConn(self.cfg, sock, client, server)
      self.nr_conns += 1
      # Register with poller - wait for data to be ready
      with self._lock:
          self.poller.register(conn.sock, selectors.EVENT_READ,
                               partial(self.on_client_socket_readable, conn))

Connections are registered with the selector first. Only when data is available (socket readable) does the connection get submitted to the thread pool. Thread slots are only used when there's actual work to do.

v25 behavior:

  def accept(self, listener):
      client_sock, client_addr = listener.accept()
      self.nr_conns += 1
      conn = TConn(self.cfg, client_sock, client_addr, listener.getsockname())
      # Submit directly to thread pool
      self.enqueue_req(conn)

Connections are immediately submitted to the thread pool after accept(). Threads then block waiting for HTTP request data to arrive.

With workers=1 and threads=4:

  • v23: 4 threads process actual requests efficiently
  • v25: 4 threads can get blocked waiting for data from slow clients or network latency...

So, most probably If the thread pool is saturated (all 4 threads waiting for data or processing requests), the health check request cannot be serviced, causing the 30-second timeout.

This need to be fixed.

Discussed in #3516

Originally posted by BehnazMoradabadi February 18, 2026

Type

Bug Report

Description

We had a container based on Django + Gunicorn 23 with these readinessProb and strartupProb and it was working fine:

          startupProbe:
            failureThreshold: 3
            httpGet:
              path: /health/liveness/
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 30
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 10
          readinessProbe:
            exec:
              command:
              - wget
              - -O
              - /dev/null
              - http://localhost:8080/health/readiness/
            failureThreshold: 3
            initialDelaySeconds: 10
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 30

after upgrading to gunicorn 25, k8s starts considering the pod unhealthy with

  Warning  Unhealthy  16s    kubelet            Readiness probe failed: command timed out: "wget -O /dev/null http://localhost:8080/health/readiness/" timed out after 30s

Rediness function does nothing except sending an empty response:

    def readiness(self, request):
        return HttpResponse()

I don't see any weird log in the pod. It seems to be working, but it gets restarted constantly.
Can you please help me how to debug this furthur. No spike in the cpu or memory or even data transfer

Steps to Reproduce (for bugs)

No response

Configuration

bind = "[::]:8080"
access_log_format = '%(h)s - %(u)s [%(t)s] "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
accesslog = "-"
disable_redirect_access_to_syslog = True
errorlog = "-"

graceful_timeout = 60

workers = 1
threads = 4
timeout = 60

Logs / Error Output

Warning  Unhealthy  16s    kubelet            Readiness probe failed: command timed out: "wget -O /dev/null http://localhost:8080/health/readiness/" timed out after 30s

Gunicorn Version

gunicorn 25.0.3

Python Version

python:3.12

Worker Class

sync (default)

Operating System

alpine3.19

Additional Context

No response

Checklist

  • I have searched existing discussions and issues for duplicates
  • I have checked the documentation and FAQ

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions