Troubleshooting
Gunicorn & app servers

Fix Gunicorn "Failed to find attribute 'application'"

Fix Gunicorn errors that cannot find the Django application object: wrong module or attribute names, shadowed modules, ASGI paths, and wsgi.py mistakes.

Problem statement

Gunicorn starts, imports a module, and then stops with:

Failed to find attribute 'application' in 'config.wsgi'.
[INFO] Worker exiting (pid: 4312)
gunicorn.errors.HaltServer: <HaltServer 'App failed to load.' 4>

or a related message:

Failed to find attribute 'app' in 'config.wsgi'.
Application object must be callable.
Failed to parse 'config.wsgi:get_application(' as an attribute name or function call.

Unlike ModuleNotFoundError, the module was imported. Gunicorn then looked for the name after the colon in the module:attribute argument and did not find a callable with that name. The cause is almost always a mismatch between the command line and what the module defines, or Gunicorn importing a different module than the one you think.

How Gunicorn loads the appCommandmodule:attributeImportconfig.wsgiAttributeapplicationWorkersserve requestsThe module imported fine; the attribute after the colon is what could not be found.

Quick answer

  1. Look at the exact module:attribute string in your Gunicorn command.
  2. Django's generated wsgi.py defines application; use config.wsgi:application (replace config with your project package).
  3. If you omit :attribute, Gunicorn looks for application by default, so config.wsgi alone also works.
  4. Make sure wsgi.py actually assigns application = get_wsgi_application() at module level and does not fail silently.
  5. Check for a shadowing module: another config or wsgi module earlier on sys.path.
  6. Do not point Gunicorn's default sync workers at asgi.py.
  7. Verify with python -c "import config.wsgi as m; print(m.application)".

Step-by-step solution

1. Check the command

systemctl cat gunicorn | grep ExecStart
# or in Docker
docker compose config | grep -A3 command
module path, then attributegunicorn--chdir /srv/appconfig.wsgi:application1231Where Python starts looking for config/.2Dotted module path, not a file path.3The variable inside wsgi.py.Failed to find attribute means the module loaded but the name is wrongdjango-deployment.com
python -c "import config.wsgi" in the same directory tests the first half.

The argument has the form MODULE:VARIABLE. For a standard project created with django-admin startproject config .:

gunicorn config.wsgi:application

Frequent mismatches:

CommandProblem
gunicorn config.wsgi:appFlask-style name; Django uses application
gunicorn config.wsgi:wsgiwsgi is the module, not the variable
gunicorn config.wsgi:get_wsgi_applicationrefers to the factory function imported into the module, not an application instance
gunicorn myproject.wsgi:application after renaming the projectimports a different module or fails to import

2. Inspect wsgi.py

A correct file:

# config/wsgi.py
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

application = get_wsgi_application()

Problems to look for:

  • application defined inside if __name__ == "__main__": or inside a function, so it does not exist when Gunicorn imports the module
  • the assignment was renamed, commented out, or removed during a merge conflict
  • a try/except around setup that swallows an exception, leaving application undefined

Check what the module exposes:

cd /srv/myapp/app
/srv/myapp/venv/bin/python -c "import config.wsgi as m; print(m.__file__); print(getattr(m, 'application', 'MISSING'))"

Expected output shows the path of your wsgi.py and a WSGIHandler object.

3. Check for a shadowed module

If m.__file__ in the previous command points somewhere unexpected, Python imported a different module with the same name. Examples:

  • a top-level directory or file named config in a dependency or in the working directory's parent
  • an old wsgi.py in a previous release directory still on PYTHONPATH
  • a stale config.pyc or __pycache__ without source in an unusual layout
the wrong module gets importedShadowed✕a config.py beside the config/ package✕or a pip package with the same name✕import finds the wrong one firstUnique name✓one module per name on sys.path✓config.wsgi.__file__ points at yours✓no stale .pyc of a deleted filePrint module.__file__ to see which one was really importeddjango-deployment.com
Common collisions: config, app, main, and settings.

Print the search path:

/srv/myapp/venv/bin/python -c "import sys; print('\n'.join(sys.path))"
env | grep PYTHONPATH

Remove stray PYTHONPATH settings from environment files and unit files unless you need them, and give the project package a distinctive name if a common name such as config collides with an installed package.

4. Application factory functions

Some projects wrap setup in a function, for example to configure monitoring before creating the app:

# config/wsgi.py
from django.core.wsgi import get_wsgi_application


def create_app():
    # additional setup here
    return get_wsgi_application()

Gunicorn supports calling a factory with parentheses in the application argument:

gunicorn "config.wsgi:create_app()"

Quote the argument so the shell does not interpret the parentheses. The simpler option is to keep a module-level application = create_app(), which works with every WSGI server and platform auto-detection.

5. Don't mix WSGI and ASGI entry points

Pointing Gunicorn's default sync workers at asgi.py:

gunicorn config.asgi:application
match the server to the entry pointResultgunicorn + wsgi✓worksgunicorn + asgi✕boots, then fails on requestsuvicorn + wsgi✕fails: not an ASGI appgunicorn -k uvicorn_worker + asgi✓worksSync Gunicorn workers need wsgi.py; ASGI servers need asgi.pydjango-deployment.com
The error text differs by combination, which is why it is easy to miss.

imports successfully, and Gunicorn finds a callable named application, but the ASGI application expects a different calling convention. Requests fail with errors such as TypeError about missing arguments. For ASGI, run Uvicorn workers explicitly:

gunicorn config.asgi:application -k uvicorn_worker.UvicornWorker

The uvicorn-worker package provides that worker class; older setups use uvicorn.workers.UvicornWorker, which is deprecated. Use wsgi.py with sync or gthread workers otherwise.

6. Platform-generated commands

Some platforms (Azure App Service, Elastic Beanstalk, Render) generate or suggest a Gunicorn command from project detection. If you renamed your project package or moved wsgi.py, the detected module path is stale. Set an explicit start command in the platform configuration.

7. Check the full startup log

The "failed to find attribute" line sometimes follows an earlier exception printed during import. Read everything from the start of the boot:

sudo journalctl -u gunicorn -b --no-pager | sed -n '/Starting gunicorn/,$p' | head -60

8. Verify

cd /srv/myapp/app
/srv/myapp/venv/bin/gunicorn --check-config config.wsgi:application && echo "config ok"
sudo systemctl restart gunicorn
sudo journalctl -u gunicorn -n 15 --no-pager

--check-config loads the configuration and application without starting workers, and exits with a non-zero status if loading fails.

Verification check:

  • --check-config prints nothing alarming and exits 0
  • the journal shows workers booting
  • requests through Nginx return responses from Django

Explanation

The WSGI specification defines an application as a callable that takes an environment and a start_response function. Gunicorn loads it by importing a module and reading one attribute from it. Django's get_wsgi_application() returns such a callable, and the project template stores it in a module-level variable named application, which is also Gunicorn's default attribute name. Any drift in the module path, the variable name, or which file Python actually imports breaks that contract.

Edge cases / notes

Error only when using --preload

With --preload, the application loads in the master process before forking. Import-time side effects (opening connections, starting threads) can fail differently than in workers. Check for code that assumes it runs in a worker.

Gunicorn configuration file sets wsgi_app

A gunicorn.conf.py can define wsgi_app = "config.wsgi:application". If both the config file and the command line specify an app, the command-line value is used; an outdated wsgi_app in the config file is harmless but confusing. Keep one source of truth.

Multiple projects in one repository

Monorepos with several Django projects often have several wsgi.py files. Make the working directory and module path unambiguous for each service.

Gunicorn configuration files as the single source of truth

Long command lines in systemd units, Dockerfiles, and platform settings drift apart. A gunicorn.conf.py committed to the repository keeps the application path and server options in one place:

# gunicorn.conf.py
import multiprocessing
import os

wsgi_app = "config.wsgi:application"
bind = os.environ.get("GUNICORN_BIND", "unix:/run/myapp/gunicorn.sock")
workers = int(os.environ.get("GUNICORN_WORKERS", multiprocessing.cpu_count() + 1))
threads = int(os.environ.get("GUNICORN_THREADS", "2"))
worker_class = "gthread"
timeout = 60
graceful_timeout = 30
max_requests = 1000
max_requests_jitter = 100
accesslog = "-"
errorlog = "-"

Gunicorn reads ./gunicorn.conf.py from the working directory by default, so the service command becomes:

/srv/myapp/venv/bin/gunicorn

or explicitly gunicorn -c /srv/myapp/app/gunicorn.conf.py. Container images, systemd units, and local testing then share the same application path, and renaming the project package means changing one line.

Diagnosing with Python directly

When the error message is not enough, mimic what Gunicorn's loader does:

cd /srv/myapp/app
/srv/myapp/venv/bin/python - <<'PY'
import importlib

module_name, _, attr = "config.wsgi:application".partition(":")
module = importlib.import_module(module_name)
print("module file:", module.__file__)
print("public names:", [n for n in dir(module) if not n.startswith("_")])
app = getattr(module, attr or "application", None)
print("found:", app, "callable:", callable(app))
PY

The list of public names shows immediately whether application exists, is misspelled, or was never assigned because setup failed earlier in the module.

Platform auto-detection

Some platforms infer the WSGI module from the project. After renaming the settings package or moving wsgi.py, update the platform's start command explicitly rather than relying on detection, and keep the configuration file above as the reference.

Common mistakes

  • Copying a Flask Gunicorn command (app:app) into a Django deployment.
  • Pointing at config.wsgi:get_wsgi_application.
  • Running sync workers against asgi.py.
  • Leaving an old PYTHONPATH in the environment file after moving releases.

Next steps

FAQ

What should the Gunicorn application argument be for Django?

Use the dotted module path to wsgi.py followed by :application, for example config.wsgi:application, run from the directory that contains manage.py.

Why does Gunicorn say it cannot find 'app' in my Django project?

The command uses :app, a name common in Flask projects. Django's wsgi.py defines application, so change the argument or omit the attribute to use the default.

Can Gunicorn serve Django's asgi.py?

Only with an ASGI worker class such as Uvicorn's worker. The default sync worker expects a WSGI application from wsgi.py.

How do I test that Gunicorn can load my Django app without starting it?

Run gunicorn --check-config config.wsgi:application from the project directory with the same virtualenv and environment variables as the service.

Keep going