How to Make Your App Wait for Postgres on Startup
When your app connects to Postgres at startup, it races the database. If Postgres isn't ready yet, the connect raises immediately and the process dies. Adding a retry-with-backoff loop is the standard fix - the app keeps trying for a few seconds rather than crashing on the first refused connection.
Why the app crashes on startup
A Flask (or any Python) app that calls psycopg2.connect() at module load time
runs into a race condition: if Postgres is still starting when the app process
launches, the connect attempt gets Connection refused and raises immediately.
The process exits before it ever serves a request.
This is especially common in Docker Compose and Kubernetes where the app
container starts in parallel with the database container. Even a depends_on
check in Compose only waits for the container to be running, not for Postgres
to be accepting connections.
The fix: retry with exponential backoff
Wrap the startup connect in a loop that retries with increasing delays:
import time
import psycopg2
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "appdb",
"user": "appuser",
"password": "apppass",
}
def wait_for_db(retries=15):
for attempt in range(retries):
try:
conn = psycopg2.connect(**DB_CONFIG)
conn.close()
print("Database ready.")
return
except psycopg2.OperationalError as e:
wait = min(2 ** attempt, 5)
print(f"DB not ready (attempt {attempt + 1}): {e} - retrying in {wait}s")
time.sleep(wait)
raise SystemExit("Could not connect to database after retries")
wait_for_db()
The key points:
- Exponential backoff -
min(2 ** attempt, 5)starts at 1 second and caps at 5, so the total wait time stays bounded (around 30 seconds for 15 attempts). - Catch only
OperationalError- this covers refused connections and authentication failures, not programming errors like a bad query. - Hard exit on exhaustion - if Postgres never comes up,
SystemExitstops the process cleanly instead of leaving a zombie that appears healthy but can't serve requests.
Verify it works
# Check Postgres is ready separately
pg_isready -U postgres
# Check the app logs for retry lines
cat /var/log/app.log
# Confirm the app is up and serving
curl http://localhost:8080/health
Going further in production
App-level retry is the safety net, not the only layer. In Kubernetes, pair it with:
livenessProbe- restarts the pod if the app stops responding after startup.readinessProbe- keeps traffic off the pod until the app signals ready (after the DB connect succeeds).- Init containers - run
pg_isreadyin an init container to block the app container from starting until Postgres accepts connections.
These layers complement each other: init containers prevent the startup race, probes handle mid-run failures, and the app-level retry catches anything the init container misses.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Adding a retry-with-backoff loop around the startup DB connect
- Reading app crash logs and pg_isready to diagnose the startup race
- Pairing app-level retry with Kubernetes readiness and liveness probes
FAQ
Why does my app crash immediately on startup with 'connection refused' from Postgres?
The app is calling psycopg2.connect() before Postgres is ready to accept connections. It's a startup race condition - the app process starts, tries to connect, gets refused, and exits. The fix is a retry loop with backoff rather than a single connect attempt.
How do I make Python wait for Postgres to be ready?
Wrap the startup connect in a loop that catches psycopg2.OperationalError, waits with exponential backoff (e.g. min(2**attempt, 5) seconds), and gives up after a reasonable number of retries. This lets the app survive a few seconds of Postgres startup time.
Does Kubernetes depends_on or init containers replace app-level retry?
Not entirely. Init containers can block the app until pg_isready passes, which prevents the startup race. But app-level retry handles cases where Postgres restarts mid-run or the init check races a slow auth. Both layers together are the standard production pattern.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →