From b1c81a35e64371bbd3fe2a265305aa600209a629 Mon Sep 17 00:00:00 2001 From: Ed Morley <501702+edmorley@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:51:42 +0100 Subject: [PATCH] Raise the Keep-Alive idle timeout to 100 seconds Heroku's Router 2.0 now supports Keep-Alive connections: https://www.heroku.com/blog/tips-tricks-router-2dot0-migration/#keepalives-always-on https://devcenter.heroku.com/articles/http-routing#legacy-router-and-router-2-0 Gunicorn supports Keep-Alive connections, and so now if an app is using Router 2.0 connections between the router instances and the app will be kept alive. However, gunicorn's default `keepalive` idle timeout setting is only 5 seconds: https://docs.gunicorn.org/en/stable/settings.html#keepalive This is shorter than the 90 second Router 2.0 keep-alive timeout: https://devcenter.heroku.com/articles/http-routing#keepalives As such: (a) This causes connections to be closed sooner than they need to be (as noted in the gunicorn docs, the default 5 seconds is more suited for cases where many clients are connecting directly to gunicorn, rather than gunicorn being behind a load balancer). (b) there is a possibility of a race condition whereby gunicorn starts initiating the closing of an idle Keep-Alive connection just at the moment that the Heroku Router sends a new request to it. If this occurred, that new request could fail. This race condition isn't unique to Heroku or gunicorn, but applies to any load balancer/app server combination that supports Keep-Alive. For example, see AWS' explanation about this in their ELB docs: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html#connection-idle-timeout Therefore, the gunicorn idle timeout has been raised from 5 seconds to 100 seconds, so it is greater than the Router's idle timeout - ensuring that the router is always initiating connection closing: https://docs.gunicorn.org/en/stable/settings.html#keepalive GUS-W-18319007. --- gunicorn.conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 09e78a5d6..f908c3a68 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -49,6 +49,13 @@ # https://devcenter.heroku.com/articles/dyno-shutdown-behavior graceful_timeout = 20 +# The number of seconds an idle Keep-Alive connection is kept open. This should be greater than +# the Heroku Router's Keep-Alive idle timeout of 90 seconds, to ensure that the closing of idle +# connections is always initiated by the router and not gunicorn, to prevent a race condition +# if the router sends a request to the app just as gunicorn is closing the connection: +# https://devcenter.heroku.com/articles/http-routing#keepalives +keepalive = 100 + # Enable logging of incoming requests to stdout. accesslog = "-"