aio-libs/aiohttp · error · RuntimeError

wsgi app should be either Application or async function…

Error message

wsgi app should be either Application or async function returning Application, got {self.wsgi}

What it means

Raised by GunicornWebWorker._run() when self.wsgi (the app object passed to gunicorn) is neither an aiohttp.web.Application instance nor a coroutine function returning an Application (or AppRunner). The gunicorn aiohttp worker only knows how to serve those two shapes; anything else is rejected at worker boot. The %s placeholder echoes the offending value.

Solutions

  1. Export an Application instance directly: `app = web.Application(); ...` and reference it in the gunicorn command.
  2. Or export an async factory that returns an Application: `async def app(): return web.Application()`.
  3. If returning a pre-configured runner, return a web.AppRunner from the coroutine (it is unwrapped at line 77-79).
  4. Verify with `inspect.iscoroutinefunction(obj)` and `isinstance(obj, web.Application)` before deploying.

Example fix

// before
# module:app
app = web.Application  # the class, not an instance -> raises

// after
# module:app
app = web.Application()
# or a factory:
async def app():
    application = web.Application()
    application.router.add_get('/', handler)
    return application
Defensive patterns

Strategy: validation

Validate before calling

import inspect
from aiohttp import web
assert isinstance(app, web.Application) or inspect.iscoroutinefunction(app), \
    f'wsgi must be Application or async factory, got {type(app)}'

Type guard

def is_valid_wsgi(obj) -> bool:
    import inspect
    from aiohttp import web
    return isinstance(obj, web.Application) or inspect.iscoroutinefunction(obj)

Try / catch

# raised at worker boot; fix the exported symbol rather than catching at runtime.
# In a startup self-test:
try:
    import module
    assert is_valid_wsgi(getattr(module, 'app'))
except (ImportError, AssertionError) as e:
    fail_build(f'gunicorn app target invalid: {e}')

Prevention

When it happens

Trigger: Pointing gunicorn at a plain function (not async), a class, a string module path, an async function that returns something other than Application/AppRunner, or a sync factory. Set via `gunicorn ... module:app` where `app` resolves to the wrong type.

Common situations: Exporting a factory function but forgetting `async def`; exporting the module or a router instead of the Application; returning a web.Server or TCPSite instead of Application/AppRunner from the factory.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/c49298eb45578a92. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/worker.py:83

            self.loop.close()

        sys.exit(self.exit_code)

    async def _run(self) -> None:
        runner = None
        if isinstance(self.wsgi, Application):
            app = self.wsgi
        elif inspect.iscoroutinefunction(self.wsgi) or (
            sys.version_info < (3, 14) and asyncio.iscoroutinefunction(self.wsgi)  # type: ignore[deprecated]
        ):
            wsgi = await self.wsgi()
            if isinstance(wsgi, web.AppRunner):
                runner = wsgi
                app = runner.app
            else:
                app = wsgi
        else:
            raise RuntimeError(
                "wsgi app should be either Application or "
                f"async function returning Application, got {self.wsgi}"
            )

        if runner is None:
            access_log = self.log.access_log if self.cfg.accesslog else None
            runner = web.AppRunner(
                app,
                logger=self.log,
                keepalive_timeout=self.cfg.keepalive,
                access_log=access_log,
                access_log_format=self._get_valid_log_format(
                    self.cfg.access_log_format
                ),
                shutdown_timeout=self.cfg.graceful_timeout / 100 * 95,
            )
        await runner.setup()

View on GitHub (pinned to d041d4d0fd)