aio-libs/aiohttp · error · RuntimeError

Changing state of started or joined application is forbidden

Error message

Changing state of started or joined application is forbidden

What it means

Raised by Application._check_frozen when self._frozen is True. An application is frozen after AppRunner startup or after explicit freeze(); at that point the _state dict, middlewares, router, and signals are immutable because they are already in use by live request handling. Mutating app[key]=, del app[key], or adding routes after startup therefore aborts. The guard keeps running requests consistent with the configuration they were dispatched under.

Source

Thrown at aiohttp/web_app.py:155

        )

    # MutableMapping API

    def __eq__(self, other: object) -> bool:
        return self is other

    @overload  # type: ignore[override]
    def __getitem__(self, key: AppKey[_T]) -> _T: ...

    @overload
    def __getitem__(self, key: str) -> Any: ...

    def __getitem__(self, key: str | AppKey[_T]) -> Any:
        return self._state[key]

    def _check_frozen(self) -> None:
        if self._frozen:
            raise RuntimeError(
                "Changing state of started or joined application is forbidden"
            )

    @overload  # type: ignore[override]
    def __setitem__(self, key: AppKey[_T], value: _T) -> None: ...

    @overload
    def __setitem__(self, key: str, value: Any) -> None: ...

    def __setitem__(self, key: str | AppKey[_T], value: Any) -> None:
        self._check_frozen()
        if not isinstance(key, AppKey):
            warnings.warn(
                "It is recommended to use web.AppKey instances for keys.\n"
                + "https://docs.aiohttp.org/en/stable/web_advanced.html"
                + "#application-s-config",
                category=NotAppKeyWarning,
                stacklevel=2,

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Move initialization into an on_startup handler or cleanup_ctx, both fire before freeze takes effect.
  2. Store per-request data on request['key'], not app['key'].
  3. Register all routes and middlewares BEFORE run_app()/AppRunner.setup().
  4. If you must reconfigure, rebuild a new Application rather than mutating a frozen one.

Example fix

// before
async def handler(request):
    request.app['db'] = Database()  # app frozen at request time
    ...
// after
async def init_db(app):
    app['db'] = Database()
app.on_startup.append(init_db)
Defensive patterns

Strategy: validation

Validate before calling

def safe_set(app, key, value):
    if getattr(app, '_frozen', False):
        raise RuntimeError('app is frozen; set state in on_startup instead')
    app[key] = value

Try / catch

try:
    app[key] = value
except RuntimeError as e:
    if 'started or joined' in str(e):
        # defer to on_startup or move to per-request storage
        ...
    raise

Prevention

When it happens

Trigger: Setting `app['cache'] = ...` inside a request handler or on_startup-after-startup; adding routes from a background task after the runner started; mutating middleware list after run_app(); storing per-request state on the app instead of on the request.

Common situations: Lazy initialization done in the wrong lifecycle hook (should be on_startup, not in a handler); plugin systems that register routes dynamically; tests that reuse one app across multiple run_app() invocations; per-request caches accidentally written to app.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/4a19add5822f3e30.json. Report an issue: GitHub.