Fix Gunicorn "Worker failed to boot"
Fix Gunicorn HaltServer 'Worker failed to boot' for Django: find the real traceback, reproduce the import with the same environment, and fix settings, imports, and startup-time side effects.
Problem statement
Gunicorn starts and immediately exits. The last lines of the log look like this:
[INFO] Booting worker with pid: 4312
[ERROR] Exception in worker process
...
[INFO] Worker exiting (pid: 4312)
[ERROR] Worker (pid:4312) exited with code 3
[ERROR] Shutting down: Master
[ERROR] Reason: Worker failed to boot.
or, as a Python exception, gunicorn.errors.HaltServer: <HaltServer 'Worker failed to boot.' 3>.
"Worker failed to boot" is not the error; it is Gunicorn reporting that a worker process crashed while loading your application, so the master shut down instead of restarting it forever. The actual cause is the traceback printed above those lines. This guide shows how to find it quickly and fixes the common causes.
- Read the full log, not the last line:
journalctl -u gunicorn -n 200 --no-pager(ordocker logs). - Look for the first traceback after "Exception in worker process".
- Reproduce the import with the same user, directory, interpreter, and environment:
python -c "import config.wsgi". - Run
python manage.py check --deployin that environment. - Fix the cause: missing environment variables, import errors, missing packages, code that touches the database or network at import time, or wrong settings module.
- Restart and confirm workers stay up.
Step-by-step solution
1. Get the full traceback
sudo journalctl -u gunicorn -n 200 --no-pager
Scroll up to the first Traceback (most recent call last): after Exception in worker process. The last line of that traceback names the real error, for example:
django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.
KeyError: 'DATABASE_URL'
ModuleNotFoundError: No module named 'whitenoise'
django.db.utils.OperationalError: connection to server at "10.0.0.5", port 5432 failed
If the log is too noisy, start Gunicorn manually in the foreground with debug logging (see step 2).
2. Reproduce exactly as the service runs
Run the import as the service user, from the service's working directory, with the service's environment:
sudo -u myapp bash -c '
cd /srv/myapp/current
set -a; . /etc/myapp.env; set +a
/srv/myapp/current/.venv/bin/python -c "import config.wsgi; print(\"import ok\")"
'
If that works, run Gunicorn itself in the foreground with one worker:
sudo -u myapp bash -c '
cd /srv/myapp/current
set -a; . /etc/myapp.env; set +a
/srv/myapp/current/.venv/bin/gunicorn config.wsgi:application --workers 1 --log-level debug --bind 127.0.0.1:8001
'
Differences between this and the systemd unit (User=, WorkingDirectory=, EnvironmentFile=, the interpreter path in ExecStart=) are the usual reason "it works when I run it" but not under systemd.
3. Fix the common causes
Missing or empty environment variables. Settings like SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] raise KeyError at import. Check that the unit loads the right file (EnvironmentFile=/etc/myapp.env), that the file is readable by root (systemd reads it before dropping privileges), and that the variable names match. The file uses KEY=value lines without export.
Wrong settings module. If DJANGO_SETTINGS_MODULE is unset, wsgi.py falls back to its default, often development settings that expect a local database or debug tools not installed in production. Set it explicitly in the environment file.
Missing packages. ModuleNotFoundError means the package is not installed in the interpreter Gunicorn uses. Check that ExecStart points at the release's virtual environment and that dependencies were installed there:
/srv/myapp/current/.venv/bin/python -m pip show whitenoise
Import-time side effects. Code that runs when a module is imported (in settings.py, models.py, apps.py ready(), or module-level code in views) and talks to the database, Redis, or an external API makes boot depend on those services. If the database is unreachable at that moment, every worker fails to boot. Move such work into functions called at request time, or make it tolerate failures.
Syntax or version errors. A SyntaxError or TypeError from a newer Python feature means the server's Python is older than the one you developed with. Check .venv/bin/python --version.
Wrong application path. Failed to find attribute 'application' or No module named 'config' indicate the module path in ExecStart or the working directory is wrong.
4. Check Django configuration
sudo -u myapp bash -c '
cd /srv/myapp/current; set -a; . /etc/myapp.env; set +a
.venv/bin/python manage.py check --deploy
'
System checks run the same setup code as Gunicorn and often report the problem more clearly.
5. Consider --preload for clearer errors
With --preload, Gunicorn imports the application in the master process before forking workers. An import error then appears once, clearly, and Gunicorn exits before starting any worker. It also saves memory through copy-on-write. The trade-off: code changes then need a full restart rather than a HUP reload, and connections opened at import time are shared by forked workers, which is unsafe for database connections (Django opens those lazily per request, so default Django code is fine).
6. Restart and verify
sudo systemctl daemon-reload
sudo systemctl restart gunicorn
sudo systemctl status gunicorn --no-pager
Verification check:
systemctl is-active gunicorn
pgrep -u myapp -f "gunicorn" | wc -l
curl -sI -H "Host: example.com" --unix-socket /run/gunicorn/gunicorn.sock http://localhost/ | head -1
Expect active, one master plus the configured number of workers, and an HTTP response from Django.
Explanation
Each Gunicorn worker imports your WSGI module, which calls django.setup(): settings are loaded, apps are imported, and ready() hooks run. Any uncaught exception during that process kills the worker with exit code 3 (WORKER_BOOT_ERROR). Gunicorn treats a boot failure as fatal rather than restarting the worker in a tight loop, so the master stops too. The fix is always in whatever made the import fail; the final "Worker failed to boot" line is only the summary.
Edge cases / notes
Timeouts during boot
If imports are slow (large apps, heavy libraries, or network calls at import), a worker may be killed by Gunicorn's --timeout before it finishes booting, logged as WORKER TIMEOUT. Remove network calls from import time rather than raising the timeout.
Out of memory at boot
On small servers, several workers booting at once can exhaust memory, and the kernel kills one. dmesg -T | grep -i "killed process" shows it. Reduce workers or add memory or swap.
When to automate this
Add python -c "import config.wsgi" and manage.py check --deploy with production settings to CI and to the deploy script before restarting Gunicorn. A release that cannot import never replaces a working one.
Next steps
- If systemd reports the service failed before Gunicorn logs anything, see Fix a Gunicorn systemd Service That Fails to Start.
- For missing packages or wrong paths, see Fix ModuleNotFoundError When Starting Django with Gunicorn.
- For a wrong WSGI target, see Fix Gunicorn "Failed to find attribute 'application'".
- For settings errors, see Fix ImproperlyConfigured: SECRET_KEY Must Not Be Empty.
FAQ
What does "Worker failed to boot" mean in Gunicorn?
A worker process raised an exception while importing the application. Gunicorn stops the master to avoid a restart loop. The traceback above that line shows the cause.
Why does Gunicorn work from my shell but fail under systemd?
The environment differs: user, working directory, environment variables, or interpreter path. Reproduce with the unit's exact settings.
What is exit code 3 in Gunicorn?
It is Gunicorn's worker boot error code, meaning the application failed to load.
Does --preload fix "Worker failed to boot"?
No, but it surfaces import errors once in the master process, which makes them easier to read.