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
- Move initialization into an on_startup handler or cleanup_ctx, both fire before freeze takes effect.
- Store per-request data on request['key'], not app['key'].
- Register all routes and middlewares BEFORE run_app()/AppRunner.setup().
- 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
- Set all app state in on_startup / cleanup_ctx, never in handlers.
- Store per-request data on request, not app.
- Register routes/middlewares before run_app().
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
- Cannot add sub application to frozen application
- Cannot add frozen application
- Cannot write to closing transport
- Session is closed
- Connection closed
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/4a19add5822f3e30.json.
Report an issue: GitHub.