Skip to content

Cannot change gunicorn keep-alive setting via GUNICORN_KEEP_ALIVE #8543

Description

@Xopherus

How are you running Flagsmith

  • Self Hosted with Docker
  • Self Hosted with Kubernetes
  • SaaS at flagsmith.com
  • Some other way (add details in description below)

Describe the bug

Summary

GUNICORN_KEEP_ALIVE is documented as a supported variable and is set in Flagsmith's own production ECS task definitions, but nothing in the codebase reads it.

This matters behind a load balancer: an idle timeout above 2s (AWS ALB defaults to 60s) means gunicorn closes pooled connections the LB still considers reusable, and the next request over that socket returns a 502 to the caller. I've been seeing bursts in 502s during deployments and sampled at ~1% at other times.

Environment

  • flagsmith-api:2.256.1 (also reproduced on the enterprise image)
  • flagsmith-common 3.12.0; code links above confirmed against current main
  • gunicorn 23.0.0, gthread worker (image sets GUNICORN_THREADS=2)

Workaround

Override the container args to put the flag before the sub-command:

args: ["start", "--keep-alive", "65", "api"]

Caveat: this bypasses serve, losing its _wait_for_db(). Deployments relying on that need an init container that waits for the database.

Suggested fixes

Any one of these would resolve it; the first is the smallest:

  1. Add "keepalive": env.int("GUNICORN_KEEP_ALIVE", 2) to GUNICORN_FLAGSMITH_DEFAULTS, matching the existing env vars and the docs.
  2. Have serve() forward extra args before the sub-command — ["flagsmith", "start", *argv, "api"] — so documented gunicorn flags are reachable from the default entrypoint.
  3. Call super().load_config() in DjangoWSGIApplication.load_config() so GUNICORN_CMD_ARGS behaves as gunicorn users expect.

If the intent is that keep-alive should not be configurable, removing the row from the docs and the variable from the ECS task definitions would at least make that explicit.

Steps To Reproduce

Steps to reproduce

1. GUNICORN_KEEP_ALIVE has no effect

Start Postgres and Flagsmith with the variable set to something well above the 2s default:

docker network create fs

docker run -d --name fs-db --network fs \
  -e POSTGRES_USER=u -e POSTGRES_PASSWORD=p -e POSTGRES_DB=db \
  postgres:15-alpine

docker run --rm --network fs \
  -e DATABASE_URL=postgresql://u:p@fs-db:5432/db -e DJANGO_SECRET_KEY=x \
  docker.flagsmith.com/flagsmith/flagsmith-api:2.256.1 migrate

docker run -d --name fs-api --network fs -p 8000:8000 \
  -e DATABASE_URL=postgresql://u:p@fs-db:5432/db -e DJANGO_SECRET_KEY=x \
  -e GUNICORN_KEEP_ALIVE=65 \
  docker.flagsmith.com/flagsmith/flagsmith-api:2.256.1 serve

Open a keep-alive connection and time how long the server keeps it:

import socket, time
s = socket.create_connection(("127.0.0.1", 8000))
s.sendall(b"GET /health/liveness/ HTTP/1.1\r\n"
          b"Host: localhost\r\nConnection: keep-alive\r\n\r\n")
t0 = time.time()
while s.recv(65536):      # read until the server closes the connection
    pass
print(f"closed after {time.time() - t0:.1f}s")

Expected: ~65s. Actual: ~2s — gunicorn's default, identical to running with no variable set at all.

Repeat with -e GUNICORN_CMD_ARGS="--keep-alive 65" (gunicorn's own native env var) for the same ~2s result.

▎ Read until the socket closes, as above, rather than sleeping and polling. The
▎ health endpoint responds with Transfer-Encoding: chunked, so a probe that
▎ doesn't consume the whole body will pick up leftover body bytes and misreport
▎ the connection as closed immediately.

2. The CLI flag works, but only before the sub-command

docker rm -f fs-api
docker run -d --name fs-api --network fs -p 8000:8000 \
  -e DATABASE_URL=postgresql://u:p@fs-db:5432/db -e DJANGO_SECRET_KEY=x \
  docker.flagsmith.com/flagsmith/flagsmith-api:2.256.1 \
  start --keep-alive 65 api

Re-run the probe: the connection now stays open well past 2s.

3. serve cannot pass the flag at all

No database needed — this fails at argument parsing:

docker run --rm -e SKIP_WAIT_FOR_DB=1 \
  -e DATABASE_URL=postgresql://u:p@127.0.0.1:5432/db -e DJANGO_SECRET_KEY=x \
  docker.flagsmith.com/flagsmith/flagsmith-api:2.256.1 serve --keep-alive 65

flagsmith start: error: unrecognized arguments: --keep-alive 65

Exit code 2 at startup, so under Kubernetes this is a CrashLoopBackOff rather than a degraded start.

Cleanup

docker rm -f fs-api fs-db && docker network rm fs

Two notes on accuracy:

  • Steps 1 and 3 are exactly what I ran; step 3's minimal no-database form I confirmed produces that error verbatim.
  • The probe snippet above is a simplified version of what produced the timing table. Mine drained the response and then timed the idle period; this one measures request-to-close, which is the same thing plus a
    few milliseconds of response time. I didn't re-run the simplified form — if you want it verified before filing, say the word and I'll spin the containers back up.

Expected behavior

I would expect keep-alive to be changed to a different value.

Screenshots

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions