Skip to main content

docs/lifespan.md

# Lifespan

Lifespans let a FastestMCP server run startup and shutdown logic exactly once
per runtime.

That is the right place for state that should exist for the lifetime of the
server process tree:

- warm configuration
- shared caches
- prepared lookup tables
- long-lived clients or pooled resources
- startup bookkeeping that tools should be able to read later

Unlike per-request dependencies, lifespan hooks do not run on every call. They
run once when the server starts and clean up when the runtime stops.

## Basic Shape

Register a lifespan with `FastestMCP.add_lifespan/2`:

```elixir
server =
  FastestMCP.server("lifespan")
  |> FastestMCP.add_lifespan(fn _server ->
    {%{"started_at" => DateTime.utc_now()},
     fn ->
       IO.puts("Shutting down")
     end}
  end)
```

An enter hook can return:

- a map
- `{:ok, map}`
- `{map, cleanup_fun}`
- `{:ok, map, cleanup_fun}`
- `nil`
- `{:ok, nil}`

The returned map becomes part of `ctx.lifespan_context`.

## Accessing Lifespan State

Tools, prompts, and resources can read merged lifespan state from the context:

```elixir
server =
  FastestMCP.server("lifespan")
  |> FastestMCP.add_lifespan(fn _server ->
    %{"config" => %{"region" => "eu-west-1"}}
  end)
  |> FastestMCP.add_tool("lifespan_info", fn _arguments, ctx ->
    ctx.lifespan_context
  end)
```

That keeps startup state visible without requiring tools to know how it was
created.

## Composing Multiple Lifespans

Multiple lifespan hooks compose in declaration order:

```elixir
server =
  FastestMCP.server("lifespan")
  |> FastestMCP.add_lifespan(fn _server ->
    %{"db" => "connected", "shared" => "first"}
  end)
  |> FastestMCP.add_lifespan(fn _server ->
    %{"cache" => "warm", "shared" => "second"}
  end)
```

The merged context becomes:

```elixir
%{
  "cache" => "warm",
  "db" => "connected",
  "shared" => "second"
}
```

Later lifespans win on key conflicts. That makes override order explicit.

## Cleanup Order

Cleanup runs in reverse order.

If you register:

1. configuration
2. cache
3. client

shutdown will clean up:

1. client
2. cache
3. configuration

That ordering matters when later startup steps depend on earlier ones.

Mounted server lifespans are entered recursively when the parent runtime starts.
Mounted handlers see their own server's `ctx.lifespan_context`; parent and
child lifespan maps are not merged together. Shutdown runs child cleanup before
parent cleanup so mounted resources are released before parent resources they
may depend on.

## Failure Behavior

Startup failures are cleaned up immediately.

If one lifespan has already entered and a later one raises or returns an
invalid result:

- server startup fails
- already-entered lifespan cleanups still run

That prevents partially-started runtimes from leaking resources.

## Lifespan vs Dependencies

Use lifespan when the state should exist once for the whole server runtime:

- a warmed lookup table
- a reusable client created during startup
- configuration loaded once

Use [Dependency Injection](dependency-injection.md) when the value should be
resolved once per request or background task.

In practice:

- lifespan is runtime-scoped
- dependencies are request-scoped
- session state is conversation-scoped

## Example: Shared Startup Context

```elixir
server =
  FastestMCP.server("lifespan")
  |> FastestMCP.add_lifespan(fn server ->
    {:ok, %{"server_name" => server.name, "cache" => "warm"},
     fn state ->
       IO.inspect({:stopping, state["server_name"]})
     end}
  end)
  |> FastestMCP.add_tool("show_context", fn _arguments, ctx ->
    %{
      server: ctx.server_name,
      lifespan: ctx.lifespan_context
    }
  end)
```

## Why This Shape

FastestMCP keeps lifespan small and predictable.

There is no hidden application container and no second lifecycle system.
Lifespans are just composable startup hooks that produce a merged context plus
reverse-order cleanup callbacks. That fits OTP well and keeps startup state
easy to inspect from tools.

## Related Guides

- [Dependency Injection](dependency-injection.md)
- [Context](context.md)
- [Onboarding](onboarding.md)