aio-libs/aiohttp · error · RuntimeError

Cannot add sub application to frozen application

Error message

Cannot add sub application to frozen application

What it means

Raised by Application._add_subapp when the parent application is already frozen. Once an app has started serving (or was explicitly frozen), its router is immutable because routes are matched against live requests; adding a subapp would alter routing mid-flight. The fix is structural: register subapps before startup, or restart the application with the new topology.

Source

Thrown at aiohttp/web_app.py:288

        reg_handler("on_startup")
        reg_handler("on_shutdown")
        reg_handler("on_cleanup")

    def add_subapp(self, prefix: str, subapp: "Application") -> PrefixedSubAppResource:
        if not isinstance(prefix, str):
            raise TypeError("Prefix must be str")
        prefix = prefix.rstrip("/")
        if not prefix:
            raise ValueError("Prefix cannot be empty")
        factory = partial(PrefixedSubAppResource, prefix, subapp)
        return self._add_subapp(factory, subapp)

    def _add_subapp(
        self, resource_factory: Callable[[], _Resource], subapp: "Application"
    ) -> _Resource:
        if self.frozen:
            raise RuntimeError("Cannot add sub application to frozen application")
        if subapp.frozen:
            raise RuntimeError("Cannot add frozen application")
        resource = resource_factory()
        self.router.register_resource(resource)
        self._reg_subapp_signals(subapp)
        self._subapps.append(subapp)
        subapp.pre_freeze()
        return resource

    def add_domain(self, domain: str, subapp: "Application") -> MatchedSubAppResource:
        if not isinstance(domain, str):
            raise TypeError("Domain must be str")
        elif "*" in domain:
            rule: Domain = MaskDomain(domain)
        else:
            rule = Domain(domain)
        factory = partial(MatchedSubAppResource, rule, subapp)
        return self._add_subapp(factory, subapp)

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Register all subapps before run_app()/AppRunner.setup() — typically at module load.
  2. For dynamic tenants, use a single catch-all route whose handler dispatches, instead of mutating the router.
  3. If topology must change, tear down the runner and start a new Application.
  4. Move registration into Application construction or a setup() called before serving.

Example fix

// before
async def handler(request):
    request.app.add_subapp('/tenant', tenant_app)  # frozen -> RuntimeError
// after
app.add_subapp('/tenant', tenant_app)  # at construction time
web.run_app(app)
Defensive patterns

Strategy: validation

Validate before calling

def safe_add_subapp(parent, prefix, subapp):
    if getattr(parent, 'frozen', False):
        raise RuntimeError('parent is frozen; add subapps before startup')
    return parent.add_subapp(prefix, subapp)

Try / catch

try:
    parent.add_subapp(prefix, subapp)
except RuntimeError as e:
    if 'frozen application' in str(e):
        # rebuild topology: tear down runner, build new app
        ...
    raise

Prevention

When it happens

Trigger: Calling parent_app.add_subapp(...) from inside a request handler or after run_app() has started; adding a subapp in on_startup (which fires during the startup sequence, after pre_freeze); dynamic plugin registration triggered by a request.

Common situations: Hot-reloading plugins in a long-running server; lazy mounting in tests that reuse a started app; misordered on_startup handlers where registration runs after freeze; multi-tenant routers added on first tenant.

Related errors


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