---
title: "Lifecycle"
section: "Framework utility"
framework: WebFluid
package: webfluid
version: "1.0.0b3"
stage: "beta"
released: "August 17, 2026"
canonical: "https://docs.webfluid.dev/latest/utils/lifecycle.md"
html: "https://docs.webfluid.dev/latest/utils/lifecycle"
audience: ai-agent
format: markdown
---

# Lifecycle


`fluid.mix()` owns the process: it opens the logging session, runs your startup hooks, serves, and
on a stop signal runs your shutdown hooks and exits. This page is the exact sequence, the arity and
ordering rules for hooks, and the one thing about it that will bite you in production.


## What `mix()` does

```python
def run(self): asyncio.run(self._start())

async def _start(self):
    log_factory.start_session()
    await self._lifecycle.run_startup()

    serve = asyncio.create_task(self._run_server())
    stop = asyncio.create_task(self._shutdown_flag.wait())
    try:
        await asyncio.wait((serve, stop), return_when=FIRST_COMPLETED)
        stop.cancel()
        if not serve.done() and self._server:
            self._server.should_exit = True
        await serve
    finally:
        await self._lifecycle.run_shutdown()
        log_factory.log("Server stopped.")
```

In order:

1. **Logging session** — handlers and level are installed from `LOG_LEVEL`.
2. **Startup phase** — every registered hook, in registration order, each wrapped in `safe_execute`
   (exceptions are logged, not raised) with a progress bar.
3. **Serve** — uvicorn on `SERVER_HOST:SERVER_PORT`, with its own signal handlers disabled.
4. **Wait** — on the shutdown flag **or** the server task, whichever finishes first.
5. **Shutdown phase** — in a `finally`, so it runs on every path, in **reverse** registration order.

Because the wait covers the server task too, a server that dies on its own — a bound port, uvicorn
calling `sys.exit(1)` — is re-raised where you can read it and the shutdown hooks still run. In beta
1 that case hung forever.

> **KNOWN BUG** — This whole sequence lives in mix(), not in the ASGI lifespan protocol. An external ASGI server that imports your app and serves it directly — uvicorn main:fluid, gunicorn — never runs your startup or shutdown hooks: no tables, no scheduler, no Additives, no frozen loader stack, no static mounts. Run WebFluid apps through wf run.

## Hooks

```python
app.startup_hook(fn)         # or @app.startup_hook
app.shutdown_hook(fn)        # or @app.shutdown_hook
```

| Property            | Startup                     | Shutdown                       |
|---------------------|-----------------------------|--------------------------------|
| Required arguments  | **0**                       | **0**                          |
| Sync or async       | both                        | both                           |
| Order               | registration order          | **reverse** registration order |
| Exceptions          | logged, execution continues | logged, execution continues    |
| Registration window | before the server starts    | before the server stops        |

`Phase.add` calls `required_arg_count(fn)` and raises
`TypeError("Startup hooks must not receive non optional arguments.")` on a mismatch — at
registration time, so the error points at the right line.

After the phase has run it is **sealed**: adding a hook raises
`RuntimeError("Startup hooks cannot be added after the server was started.")`

```python
from webfluid import Fluid
from webfluid.core.ext import db


def prepare_fluid() -> Fluid:
    app = Fluid(__name__)

    from fluid.app import app_router
    from fluid.api import api_router
    app.include_router(app_router)
    app.include_router(api_router)

    from fluid.events.models import register as register_events
    app.startup_hook(register_events)

    @app.startup_hook
    async def create_tables():
        bind = db.get_bind_for_model(db.Model)
        db.Model.metadata.create_all(bind.sync_engine)

    @app.shutdown_hook
    def goodbye():
        from webfluid.utils.logging import factory as log
        log.log("The app is cooling down.")

    return app


if __name__ == "__main__":
    prepare_fluid().mix()
```

> **RULE** — Register hooks while building the app, in the factory. Never from inside a request handler — the phase is sealed by then and it raises.

> **RULE** — Hooks run sequentially, and a slow hook holds up every hook after it and delays the first request. Keep them focused: register things, warm one cache, create tables. Put long-running work behind the scheduler or an event.

## What the framework itself registers

Your hooks join the same queue these use, which is why `wf run` shows a progress bar sized to the
total:

| Phase    | Hook                                                                                                                                 | Condition                     |
|----------|--------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
| startup  | `register_additives(fluid)`                                                                                                          | `WF_ADDITIVES`                |
| startup  | `scheduler.start`                                                                                                                    | `EXT_SCHEDULING`              |
| startup  | Babel translation flush                                                                                                              | `EXT_BABEL`                   |
| startup  | JWT secret rotation                                                                                                                  | `EXT_JWT`                     |
| startup  | `events.create_pending_loops` — wire up channels declared before the loop existed                                                    | `EXT_EVENTS`                  |
| startup  | `fluid._prepare` — freeze static prefixes, mount static, freeze sources, add proxy-headers middleware, freeze the Jinja loader stack | always                        |
| startup  | `frontend.cover_fluid`                                                                                                               | `APP_FRONTEND` is not `None`  |
| startup  | Tailwind compile                                                                                                                     | `WF_TAILWIND`                 |
| startup  | Vite dev server / production build + mount                                                                                           | `APP_FRONTEND.type == "vite"` |
| shutdown | `close_proxy_client`                                                                                                                 | always                        |
| shutdown | `SQLAlchemy.dispose` — dispose the sync and async engine of every bind                                                               | `EXT_SQLALCHEMY`              |
| shutdown | Vite dev server stop                                                                                                                 | debug + vite                  |

> **WARNING** — Order within the startup phase matters and is not fully under your control: _prepare freezes the sources, the static prefixes and the loader stack, and it is registered during Fluid.__init__ — so a hook you add in the factory runs after it. Do not call add_source, add_template_loader or static_prefixes.add from a hook; call them directly during app assembly.

## Graceful shutdown

The runtime installs handlers for `SIGINT` and `SIGTERM` (plus `SIGBREAK` on Windows) from a startup
hook. A `Ctrl+C` or a container stop sets the shutdown flag rather than killing the process:

```text
signal -> flag set -> uvicorn should_exit -> in-flight request finishes
       -> shutdown hooks (reverse order) -> "Server stopped."
```

Under `wf run` there is a second layer: the CLI process forwards `SIGINT`/`SIGTERM` to the child and
gives it 10 seconds before killing it.

> **RULE** — Put anything that must survive a restart into a shutdown hook: flushing a buffer, closing a third-party client, deregistering from a service discovery. But keep it short — the 10 second grace period is the budget for the whole phase.

## Startup work that is not a hook

| Do this at                             | For                                                                          |
|----------------------------------------|------------------------------------------------------------------------------|
| Module import time (`fluid/config.py`) | Config classes                                                               |
| Factory body, before `return app`      | Routers, sources, template loaders, extensions, `scheduler.add_job`          |
| `startup_hook`                         | Events and queries, table creation, cache warming, anything needing the loop |
| `additive.before_enable`               | An Additive's routes, contracts and jobs                                     |
| `shutdown_hook`                        | Flushing and closing                                                         |

## Next

- [`utils/runtime.md`](/latest/utils/runtime.md) — what happens between boot and shutdown.
- [`cli/run.md`](/latest/cli/run.md) — the process that drives all of this.
- [`ext/events.md`](/latest/ext/events.md) — the battery whose registration must be a hook.


---

## Navigation


- [Overview](/latest/.md) — what WebFluid is, how to navigate these docs, release state and known bugs
- [Getting Started](/latest/get-started.md) — install, minimal app, the layout the framework expects

**Configuration**
- [App configs](/latest/config/app-config.md) — `app_configs/<app>.ini`, switches, `*_FILE` secrets, `[dev]`
- [Config classes](/latest/config/config-class.md) — `register_config`, `DefaultConfig`, every framework key

**Extensions**
- [Introduction](/latest/ext/base.md) — `FluidExtension`, entry points, `expand_fluid`
- [Scheduling](/latest/ext/scheduling.md) — APScheduler `AsyncIOScheduler`
- [SQLAlchemy](/latest/ext/sqlalchemy.md) — `Model`, executors, binds, sessions
- [Migrate](/latest/ext/migrate.md) — Alembic wrapper, `prepare_fluid`, single/multi-db
- [Mail](/latest/ext/mailman.md) — SMTP, sync/async, batching, attachments
- [Babel](/latest/ext/babel.md) — gettext, domains, locale selection, formatters
- [Security](/latest/ext/security.md) — users, gates, CSRF, tokens, OAuth, bearer grants
- [Events](/latest/ext/events.md) — signals, events, queries, browser socket
- [Cache](/latest/ext/cache.md) — redis / legacy backends
- [JWTManager](/latest/ext/jwt.md) — encode/decode, rotation, revocation

**Frontend (surface)**
- [Introduction](/latest/surface/tooling.md) — feature switches, `fluid_base.html`, Tailwind, themes
- [Integration](/latest/surface/frontend.md) — `APP_FRONTEND`, htmx, Vite workspaces
- [Template resolution](/latest/surface/jinja.md) — loader order, namespaces, overriding

**Additives**
- [Introduction](/latest/additives/intro.md) — manifest, routers, enabling
- [Base Additives](/latest/additives/base.md) — `type: base`, `import_base`, extension semantics
- [Interaction between](/latest/additives/contract.md) — events/queries as contracts, requirements, packaging

**Framework utility**
- [Lifecycle](/latest/utils/lifecycle.md) — `mix()`, startup/shutdown hooks, graceful stop
- [Runtime](/latest/utils/runtime.md) — `FluidContext`, request hooks, themes, proxy, rate limits
- [Logging](/latest/utils/logging.md) — the log factory, levels, files, additive attribution

**CLI**
- [Introduction](/latest/cli/wf.md) — command tree, extension CLIs
- [Create](/latest/cli/create.md) — `wf create project/app/additive`
- [Run](/latest/cli/run.md) — `wf run`, flags, debug and interactive mode
- [Ocean](/latest/cli/ocean.md) — search, install, publish

**Reference**
- [Overview](/latest/ref.md) — package layout, stability contract
- [Core](/latest/ref/core.md) — `Fluid`, config, context, `core.ext`, constants
- [Extensions](/latest/ref/extensions.md) — the battery API surface
- [Surface](/latest/ref/surface.md) — `Frontend`, Node and Tailwind tooling
- [Additives](/latest/ref/additives.md) — `Additive`, `Router`, `Manifest`, registry helpers
- [Utils](/latest/ref/utils.md) — helpers, logging, exceptions