How are you running Flagsmith
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:
- Add
"keepalive": env.int("GUNICORN_KEEP_ALIVE", 2) to GUNICORN_FLAGSMITH_DEFAULTS, matching the existing env vars and the docs.
- Have serve() forward extra args before the sub-command —
["flagsmith", "start", *argv, "api"] — so documented gunicorn flags are reachable from the default entrypoint.
- 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
How are you running Flagsmith
Describe the bug
Summary
GUNICORN_KEEP_ALIVEis 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
Workaround
Override the container args to put the flag before the sub-command:
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:
"keepalive": env.int("GUNICORN_KEEP_ALIVE", 2)toGUNICORN_FLAGSMITH_DEFAULTS, matching the existing env vars and the docs.["flagsmith", "start", *argv, "api"]— so documented gunicorn flags are reachable from the default entrypoint.super().load_config()inDjangoWSGIApplication.load_config()soGUNICORN_CMD_ARGSbehaves 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_ALIVEhas no effectStart Postgres and Flagsmith with the variable set to something well above the 2s default:
Open a keep-alive connection and time how long the server keeps it:
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
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:
Exit code 2 at startup, so under Kubernetes this is a CrashLoopBackOff rather than a degraded start.
Cleanup
Two notes on accuracy:
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