---
title: "FastAPIのlifespanの使い方｜on_eventからの移行とDB接続・モデル読込をコードで解説"
url: "https://www.issoh.co.jp/tech/details/19739/"
published: 2026-11-14
updated: 2026-11-14
categories: ["開発"]
publisher: "株式会社一創"
---

# FastAPIのlifespanの使い方｜on\_eventからの移行とDB接続・モデル読込をコードで解説

FastAPIの`lifespan`は、アプリが最初のリクエストを受ける前に1回だけ動く処理と、停止する直前に1回だけ動く処理を、1つの関数にまとめて書く仕組みです。DBの接続プール、外部APIのHTTPクライアント、機械学習モデルのように、起動時に用意して全リクエストで共有したい資源の置き場所になります。この記事では、FastAPI 0.162.7で接続プールとモデルを`lifespan`で読み込み、`request.state`からエンドポイントへ渡す実装を示す構成です。非推奨の`@app.on_event("startup")`からの移行手順、ルーターやマウントしたアプリでの動作範囲、テストやDockerで終了処理が動かない原因の切り分けも扱います。

## まとめ：FastAPIのlifespanで起動処理と終了処理を書くときの結論

新しく書くコードでは`on_event`を使わず、`@asynccontextmanager`を付けた関数を`FastAPI(lifespan=lifespan)`に渡します。`yield`より前が起動処理、後ろが終了処理です。Starletteは1.0で`on_event`を削除しており、FastAPIが互換のために自前で残している状態です。

共有する資源は`yield`で辞書として返し、エンドポイントでは`request.state`から受け取ります。グローバル変数に入れるよりテストで差し替えやすく、型の指定も可能です。`lifespan`を渡した時点で`on_event`のハンドラは呼ばれなくなるため、移行は一度に済ませます。

置くのは「アプリ全体で1つ持つもの」だけです。リクエストごとのDBセッションは`Depends`の`yield`依存に、テーブル作成はマイグレーションに回します。テストでは`TestClient`を`with`文で使い、DockerではCMDをexec形式で書かないと、`lifespan`が動かないまま気付けません。

## FastAPI 0.162系でlifespanを動かす検証環境と最小コードの書き方

まずは起動と終了のタイミングを、ログで目に見える形にして確かめます。

### fastapi\[standard\]0.162.7とSQLAlchemy 2.1の導入手順

2026年11月14日時点のPyPIで、[FastAPIの最新は0.162.7](https://pypi.org/project/fastapi/)（2026年11月13日公開）です。動作要件はPython 3.11以上に上がっています。後半でDBの接続プールを使うため、SQLAlchemy 2.1系と非同期のSQLiteドライバaiosqliteも一緒に入れます。FastAPIの起動そのものが初めてなら、[FastAPIの最短の起動手順とasync/defの使い分け](https://www.issoh.co.jp/tech/details/3859/)を先に済ませてください。

```
# 仮想環境を作り、FastAPIと非同期DBドライバ、テスト用のpytestを入れる
python3 -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]==0.162.7" "sqlalchemy[asyncio]==2.1.8" aiosqlite pytest

# main.py を置いたディレクトリで開発サーバーを起動する
fastapi dev main.py
```

`fastapi[standard]`には開発サーバーのUvicornと、HTTPクライアントのhttpxが含まれます。後の例で外部API用のクライアントを`lifespan`で作るときに、このhttpxを使います。

### asynccontextmanagerのyieldを境に起動と終了を分ける基本形

[公式のLifespanイベント](https://fastapi.tiangolo.com/ja/advanced/events/)に沿った最小の形は次のとおりです。

```
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    print("起動処理：最初のリクエストより前に1回だけ動く")
    yield
    print("終了処理：停止の直前に1回だけ動く")


app = FastAPI(lifespan=lifespan)


@app.get("/")
def root():
    return {"ok": True}
```

`fastapi dev main.py`で起動すると、Uvicornの「Waiting for application startup.」の直後に起動処理の行が出て、そのあと「Application startup complete.」が続きます。Ctrl+Cで止めると、終了処理の行が出てからプロセスが終わります。

[Starletteのlifespanの説明](https://starlette.dev/lifespan/)にあるとおり、起動処理が終わるまでリクエストは受け付けません。モデルの読み込みに30秒かかるなら、その30秒はヘルスチェックにも応答しない時間になります。`yield`の前に置く処理の重さは、そのまま起動時間です。

## lifespanでDB接続プールとMLモデルを読みrequest.stateで渡す実装

ここからは、DBの接続プール、外部API用のHTTPクライアント、推論モデルの3つを`lifespan`で用意し、エンドポイントで使うAPIを1ファイルで書きます。

```
# main.py
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Annotated, TypedDict

import httpx
from fastapi import Depends, FastAPI, Request
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine


class State(TypedDict):
    engine: AsyncEngine
    http: httpx.AsyncClient
    model: dict


def load_model() -> dict:
    # 実際は joblib.load などで学習済みの重みを読む。ここでは係数だけを持つ
    return {"coef": 42.0}


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[State]:
    engine = create_async_engine("sqlite+aiosqlite:///./app.db")
    try:
        async with httpx.AsyncClient(timeout=10.0) as http:
            yield {"engine": engine, "http": http, "model": load_model()}
    finally:
        # 接続プールを閉じる。呼ばないと終了時に警告が出る
        await engine.dispose()


app = FastAPI(lifespan=lifespan)


def get_engine(request: Request) -> AsyncEngine:
    return request.state.engine


EngineDep = Annotated[AsyncEngine, Depends(get_engine)]


@app.get("/health/db")
async def db_health(engine: EngineDep):
    async with engine.connect() as conn:
        value = await conn.scalar(text("SELECT 1"))
    return {"db": value}


@app.get("/predict")
def predict(x: float, request: Request):
    return {"result": x * request.state.model["coef"]}
```

起動したら、別の端末から次のように呼びます。

```
# 接続プールから1本借りて SELECT 1 を返す
curl "http://127.0.0.1:8000/health/db"

# lifespan で読み込んだ係数を使って推論する
curl "http://127.0.0.1:8000/predict?x=2"
```

### lifespan stateの辞書をrequest.stateへ渡す仕組み

`yield`に辞書を渡すと、その中身が各リクエストの`request.state`に入ります。FastAPIは0.94.0（2023年3月）でこの「lifespan state」に対応しました。[ASGIのLifespan Protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)は、サーバーがこの状態を各リクエストへ浅いコピーで渡すと定めています。

浅いコピーなので、`request.state.model = 別の値`と代入しても他のリクエストには影響しません。一方、辞書やクライアントの中身を書き換えると全リクエストに見えます。`lifespan`で渡すのは、接続プールやクライアントのように「共有してよいもの」に限ります。

`app.state.engine = engine`と書く古い方法も動きますが、アプリのオブジェクトをimportしないと取り出せないため、テストで差し替えにくくなります。上の例では`get_engine`という依存関数を1枚挟みました。エンドポイントは`request.state`の存在を知らずに済み、テストでは[Dependsのdependency\_overridesでの差し替え](https://www.issoh.co.jp/tech/details/19720/)がそのまま使えます。

### create\_async\_engineのプール終了時にdisposeを待つ理由

非同期エンジンは、作った`lifespan`の終了処理で`await engine.dispose()`を呼んで閉じます。[SQLAlchemyのasyncioのドキュメント](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)は、`AsyncEngine`がスコープを外れる前に`dispose()`を`await`で呼ぶことを推奨する内容です。ガベージコレクションの中では`await`できないため、閉じ忘れると「Event loop is closed」に似た`RuntimeError`の警告が出ます。

上の例で`try`と`finally`を使っているのは、`yield`の後ろで例外が起きても`dispose()`まで到達させるためです。PostgreSQLのように接続数の上限があるDBでは、`create_async_engine(url, pool_size=10, max_overflow=5)`のようにプールの大きさもここで決めます。セッションの作り方と`commit`の置き場所は、[SQLAlchemy 2.1でのセッション設計を実測した解説](https://www.issoh.co.jp/tech/details/4347/)に譲ります。

### MLモデルの読込がワーカー数の分だけメモリを使う前提と対処方法

`lifespan`は、アプリ全体で1回ではなく、ワーカーのプロセスごとに1回動きます。`fastapi run --workers 4`で起動すると、モデルの読み込みも4回走り、メモリも4倍使います。1GBのモデルなら4GBです。

対処は2つあります。1つは、ワーカー数をメモリから逆算して決めることです。もう1つは、推論を別のサーバー（専用の推論サービスや、GPUを持つ別コンテナ）に分け、FastAPIの`lifespan`ではそこへのHTTPクライアントだけを作ることです。モデルが数百MBを超えるなら、後者を選びます。ワーカー数と再起動の運用は、[Uvicornのワーカー数と本番設定の解説](https://www.issoh.co.jp/tech/details/4843/)で扱っています。

## 非推奨on\_eventからlifespanへの移行手順とStarlette 1.0の影響

古い記事や既存のコードには`@app.on_event("startup")`がまだ多く残っています。動くうちに書き換えておく理由と手順をまとめます。

### Starlette 1.0がon\_eventを削除しFastAPI側で残した経緯

FastAPIの土台であるStarletteは、1.0.0rc1で`on_startup`と`on_shutdown`の引数、`on_event()`、`add_event_handler()`を削除しました（[Starletteのリリースノート](https://starlette.dev/release-notes/)）。FastAPIはその前に、[on\_eventを自前で実装し直すPR](https://github.com/fastapi/fastapi/pull/14851)を0.128.3で取り込んでいます。経緯を[FastAPIのリリースノート](https://fastapi.tiangolo.com/release-notes/)と合わせて表にします。

| 版（公開日）                   | lifespanとon\_eventに関する変更 |
| ------------------------ | ------------------------ |
| FastAPI 0.93.0（2023-03）  | lifespan引数を追加            |
| FastAPI 0.94.0（2023-03）  | lifespan stateに対応        |
| FastAPI 0.112.2（2024-08） | ルーターのlifespanを合成         |
| FastAPI 0.128.3（2026-02） | on\_eventを自前で再実装         |
| Starlette 1.0.0（2026-03） | on\_eventと関連引数を削除        |

FastAPI 0.162.7のソースでも、`on_event`には「deprecated, use lifespan event handlers instead」という非推奨の印が付いたままです。ソースのコメントには、lifespanの仕組みが改善されたら削除する旨のTODOも残っています。今は動いても、次の大きな版で消える前提で書き換えます。

### startupとshutdownの組をlifespanの1関数へ書き換える手順

移行前のよくある形は、起動で作ったものを`app.state`に入れ、終了で閉じる2つの関数です。

```
# 移行前：非推奨の on_event
@app.on_event("startup")
async def startup():
    app.state.engine = create_async_engine(DB_URL)

@app.on_event("shutdown")
async def shutdown():
    await app.state.engine.dispose()
```

```
# 移行後：lifespan の1関数にまとめる
@asynccontextmanager
async def lifespan(app: FastAPI):
    engine = create_async_engine(DB_URL)
    try:
        yield {"engine": engine}
    finally:
        await engine.dispose()

app = FastAPI(lifespan=lifespan)
```

書き換えの手順は次のとおりです。

1. `startup`の中身を`yield`の前へ、`shutdown`の中身を`finally`の中へ移す
2. `app.state.xxx = ...`をローカル変数に変え、`yield`の辞書で返す
3. エンドポイント側の`request.app.state.xxx`を`request.state.xxx`に置き換える
4. `on_event`のデコレーターをすべて消し、`FastAPI(lifespan=lifespan)`を渡す

起動処理と終了処理を1つの関数にまとめ、同じローカル変数を使えるため、`app.state`を経由した受け渡しが要らなくなります。

### lifespanを渡すとon\_eventのハンドラが呼ばれない混在の落とし穴

移行の途中で`lifespan`と`on_event`を混ぜると、`on_event`の方は黙って呼ばれなくなります。公式も「lifespanを渡すと、startupとshutdownのイベントハンドラはもう呼ばれない。すべてlifespanか、すべてイベントかのどちらかで、両方ではない」と警告しています。

起動時にはエラーが出ず、`startup`で`app.state`に入れるはずだった値を読んだ最初のリクエストで`AttributeError`になる、という分かりにくい壊れ方をします。ライブラリが`on_event`で初期化を登録している場合も、`lifespan`を足した途端にその初期化が動かなくなる仕組みです。移行したら`grep -rn "on_event" .`で残りが無いことを確かめ、ライブラリ側は`lifespan`を提供していないか、各ライブラリの最新版の説明を確認します。

## APIRouterのlifespanとマウントしたサブアプリで起動処理が動く範囲

`lifespan`はアプリ本体だけでなく、ルーターにも渡せます。一方、`app.mount()`で載せた別アプリでは動きません。

### include\_routerで合成されるルーターのlifespanの順序と状態

`APIRouter(lifespan=router_lifespan)`のようにルーターへ渡した`lifespan`は、`app.include_router(router)`の時点でアプリ側と合成されます。FastAPIは0.112.2でこの合成に対応しました。0.162.7のソースでは、アプリ側の`lifespan`が外側、ルーター側が内側に入ります。

つまり起動はアプリ側、ルーター側の順で、終了はその逆です。ルーターの初期化でアプリ側の接続プールを使いたい場合でも、プールは先にできています。両方の`yield`が辞書を返したときは1つに合成され、同じキーがある場合に残るのはアプリ側の値です。`"engine"`のような汎用の名前は衝突しやすいため、ルーター側は`"orders_cache"`のように機能名を付けます。ルーターの分け方そのものは、同じFastAPIの記事群のAPIRouter編で扱う予定です。

### app.mountしたサブアプリのlifespanが呼ばれない制約と回避

[公式のSub Applications – Mounts](https://fastapi.tiangolo.com/advanced/sub-applications/)のとおり、`lifespan`はメインのアプリでしか実行されません。`app.mount("/v2", sub_app)`で載せた`sub_app`に`lifespan`を書いても、その中身は動かない仕組みです。

マウントするアプリが初期化を必要とするなら、その`lifespan`をメインのアプリ側から呼びます。代表例がMCPサーバーで、FastMCPの`http_app()`をマウントするときは`FastAPI(lifespan=mcp_app.lifespan)`と渡さないとセッション管理が初期化されません。既存の`lifespan`と束ねる方法も含め、[FastMCPを既存FastAPIに同居させる手順](https://www.issoh.co.jp/tech/details/6507/)で扱っています。

## 起動失敗・テスト・Docker停止時にlifespanが動かない原因の切り分け

「`lifespan`に書いたのに動いていない」という相談の多くは、次の3つのどれかです。

### Uvicornが起動例外でApplication startup failedを出す理由

`yield`の前で例外が起きると、Starlette 1.10.0は`lifespan.startup.failed`というメッセージをサーバーへ送ってから例外を送り出します。Uvicorn 0.54.0はこれを受けて「Application startup failed. Exiting.」と出力し、プロセスを終了します。ASGIの仕様どおりの動きです。

この挙動は好都合です。DBに接続できないまま起動して全リクエストが500になるより、起動の段階で落ちた方が、コンテナの再起動やデプロイの失敗として早く気付けます。起動時にDBへ`SELECT 1`を1回投げて、つながらなければ例外にしておくと、設定の誤りを本番に出す前に止めることが可能です。逆に、外部APIが一時的に落ちているだけで起動を止めたくない処理は、`try`で受けてログに残し、起動は続けます。

### TestClientをwith文で使わないとlifespanが実行されない理由

テストで`client = TestClient(app)`と書くだけでは、`lifespan`は動きません。[公式のTesting Events](https://fastapi.tiangolo.com/advanced/testing-events/)のとおり、`with`文で包んだときだけ、入るときに起動処理、抜けるときに終了処理が走ります。

```
# test_main.py
from fastapi.testclient import TestClient

from main import app


def test_predict():
    # with に入ると lifespan の起動処理が、抜けると終了処理が走る
    with TestClient(app) as client:
        res = client.get("/predict", params={"x": 2})
        assert res.json() == {"result": 84.0}
```

`with`を付け忘れると、`request.state`に`model`が入らないままエンドポイントが呼ばれ、テストが失敗します。本番では動くのにテストだけ落ちる場合は、まずここを疑います。

### DockerのCMDをshell形式で書くと終了処理が飛ばされる問題

[公式のDockerの説明](https://fastapi.tiangolo.com/deployment/docker/)は、CMDを必ずexec形式で書くよう警告しています。shell形式ではシェルが先に立ち上がり、`docker stop`の停止シグナルがFastAPIまで届かないことがあります。その場合は既定の10秒後に強制終了され、`lifespan`の終了処理は動きません。

```
# 正：exec形式。停止シグナルが FastAPI に届き、終了処理が動く
CMD ["fastapi", "run", "main.py", "--port", "80"]

# 誤：shell形式。終了処理が飛ばされることがある
CMD fastapi run main.py --port 80
```

Dockerfile全体の書き方は[FastAPIをDockerで動かす手順と本番構成](https://www.issoh.co.jp/tech/details/2980/)で扱っています。

## lifespanに書くべき処理とDependsのyield依存へ回す処理の判断基準

`lifespan`も`Depends`の`yield`依存も、「前で準備して後ろで片付ける」という同じ形をしています。違いは寿命です。

### アプリ全体で1つ持つ資源とリクエストごとの資源を分ける線引き

判断は寿命で決めます。プロセスが動いている間ずっと1つを共有するもの（接続プール、HTTPクライアント、読み込んだモデル、設定値）は`lifespan`です。リクエストごとに作って捨てるもの（DBセッション、トランザクション、認証済みのユーザー）は`Depends`の`yield`依存に置きます。

| 資源                    | 置き場所            |
| --------------------- | --------------- |
| 接続プール（AsyncEngine）    | lifespan        |
| httpx.AsyncClient     | lifespan        |
| 学習済みモデル               | lifespan        |
| DBセッション（AsyncSession） | Dependsのyield依存 |
| 認証済みユーザー              | Depends         |

よくある失敗は、`httpx.AsyncClient`をエンドポイントの中で毎回作り、リクエストのたびに接続を張り直すことです。既存のAPIで起動処理や接続の管理を組み直したい、FlaskなどからFastAPIへの移行を相談したい場合は、[API開発・システム連携の受託開発](https://www.issoh.co.jp/service/system/api/)で受け付けています。

### lifespanに入れると失敗するcreate\_allと長時間のバックグラウンド処理

`lifespan`の中で`Base.metadata.create_all()`を呼ぶ例はよく見ますが、本番では避けます。`create_all()`は無いテーブルを作るだけで、列の追加や型の変更を反映しません。ワーカーが4つあれば4回同時に走ります。テーブルの作成と変更は、Alembicのマイグレーションをデプロイの工程で1回だけ実行します。

もう1つは、`yield`の前で`await`する重い処理です。数分かかる集計やキャッシュの温めを入れると、その間はヘルスチェックにも応答せず、オーケストレーターに「起動しない」と判断されて再起動が繰り返されます。定期実行のような長い処理は、`lifespan`から`asyncio.create_task()`で起動して、終了処理で`cancel()`するか、CeleryやCronなど別のプロセスに出します。

## よくある質問

FastAPIの`lifespan`について、実装の途中で出てくることの多い質問に、2026年11月時点の版で答えます。

### lifespanは同期関数（def）で書けますか？

FastAPI 0.162.7のソースは、`yield`を含む普通の`def`の関数も包んで受け付けます。ただし、公式ドキュメントの例はすべて`@asynccontextmanager`と`async def`の組で、非同期の接続プールやクライアントを閉じる際に必要なのは`await`です。中で同期の処理しかしない場合でも、`async def`で書いておけば後から非同期の資源を足すときに書き換えずに済みます。

### –reloadで起動するとlifespanは何回実行されますか？

コードを保存するたびに、サーバーのプロセスが作り直されるため、`lifespan`の起動処理と終了処理もその都度走ります。モデルの読み込みが重いなら、開発時だけ小さなダミーのモデルを読む設定にします。`--reload`と`--workers`は[Uvicornの設定](https://uvicorn.dev/settings/)上、同時に使えません。

### uvicornの–lifespanオプションは何を指定すればよいですか？

既定の`auto`のままで構いません。FastAPIは`lifespan`のプロトコルに対応しているため、`auto`で起動処理と終了処理が実行されます。`off`を付けると`lifespan`そのものが動かなくなるので、FastAPIでは使いません。`off`が要るのは、Djangoのように`lifespan`を実装していないアプリで警告を消したい場合です。

### lifespanの中でバックグラウンドタスクを動かしてよいですか？

短い定期処理なら動かせます。起動処理で`task = asyncio.create_task(loop())`と作り、`yield`の後ろで`task.cancel()`して止めます。ワーカーの数だけタスクも増えるため、「全体で1回だけ実行したい」処理には向きません。WebSocketの配信でRedisを購読するタスクを`lifespan`で起動する例は、[FastAPIのWebSocket実装の解説](https://www.issoh.co.jp/tech/details/19697/)で扱っています。

### lifespanで作った資源をテストで差し替えるにはどうしますか？

エンドポイントが`request.state`を直接読まず、依存関数を経由する形にしておきます。この記事の`get_engine`のように1枚挟めば、テストでは`app.dependency_overrides[get_engine]`に、テスト用のSQLiteをつないだエンジンを返す関数を入れるだけで差し替えが可能です。`lifespan`ごと差し替えたい場合は、テスト用の`lifespan`を渡した別の`FastAPI`を作ってルーターを載せ直します。

## 関連記事

- [FastAPIとは？最短の起動手順とasync/defの使い分け、0.162の変更点](https://www.issoh.co.jp/tech/details/3859/)：FastAPI単体の入門と起動手順です。
- [Uvicornとは？読み方・Gunicornとの違いと起動・本番設定【0.54対応】](https://www.issoh.co.jp/tech/details/4843/)：lifespanを呼び出すASGIサーバー側の設定です。
- [FastAPIのDependsの使い方｜依存性注入・yield・テストの差し替えをコードで解説](https://www.issoh.co.jp/tech/details/19720/)：リクエスト単位の資源を扱うyield依存です。
- [FastAPIのベストプラクティス｜ディレクトリ構成・SQLAlchemy 2.1の設計・テストを実測で解説](https://www.issoh.co.jp/tech/details/4347/)：lifespanで作るエンジンを使ったセッション設計です。
- [FastMCPとは？PythonでMCPサーバーを作る手順とFastMCP 4.1の使い方【2026年10月】](https://www.issoh.co.jp/tech/details/6507/)：マウントしたMCPアプリにlifespanを渡す例です。

---

出典: [FastAPIのlifespanの使い方｜on\_eventからの移行とDB接続・モデル読込をコードで解説](<https://www.issoh.co.jp/tech/details/19739/>)（株式会社一創）
