はじめに
2026年7月25日 FastAPI 0.140.0にてメモリ効率が改善されたとのこと。
今回の 0.140.0 の改善は FastAPI 本体の依存性注入(Depends)まわりのメモリ削減(#16049「Reduce memory usage in dependencies」)。pydantic-core(Rust)の改善とは別物。
-
効果が出やすい負荷:
Dependsを多用する/ネストが深いアプリのメモリ使用量 - 効きにくい負荷: 単純なエンドポイント、PDF / Pillow などネイティブ変換のメモリ
補足:Pydantic の Rust コア(pydantic-core)による JSON 検証・シリアライズの高速化は、これより前の 0.130.0 系(#14962)で入っている。
試してみる。
環境
ハードウェア
OS Windows 11 wsl ubuntu 24.04
CPU Intel Core i7-10700F
メモリ 48GB DDR4
更新前の環境
- FastAPI 0.139.2
更新後の環境
- FastAPI 0.140.0
補足:計測は uvicorn を 1 プロセス(
--workers指定なし)で起動。依存解決は CPU を使う処理なので、実質 1 コアで直列に捌く構成。後述の RPS/Latency はこの前提の数字。
まずは計測用の処理を実装
router.py
from typing import Any
from fastapi import APIRouter, Depends
from app.features.deps.deps import ROUTE_COUNT, graph_meta, root_dependency
from app.features.deps.schemas import DepsBenchResponse
router = APIRouter(tags=["deps"])
def _to_response(root: dict[str, Any]) -> DepsBenchResponse:
tip = int(root.get("tip", 0))
return DepsBenchResponse(
depth=graph_meta["depth"],
width=graph_meta["width"],
route_count=graph_meta["route_count"],
leaf_sum=int(root.get("sum", tip)),
chain_tip=tip,
)
@router.get("/deps", response_model=DepsBenchResponse)
def read_deps(root: dict[str, Any] = Depends(root_dependency)) -> DepsBenchResponse:
"""ネストした Depends グラフを解決して返す(メイン計測用)。"""
return _to_response(root)
def _register_mirror_routes() -> None:
"""同一グラフを参照するルートを増やし、アプリ構築時の Dependant 数を膨らませる。"""
for index in range(ROUTE_COUNT):
def make_endpoint(route_index: int) -> Any:
def mirror(
root: dict[str, Any] = Depends(root_dependency),
) -> DepsBenchResponse:
return _to_response(root)
mirror.__name__ = f"read_deps_mirror_{route_index}"
mirror.__qualname__ = f"read_deps_mirror_{route_index}"
return mirror
router.add_api_route(
f"/deps/r{index}",
make_endpoint(index),
methods=["GET"],
response_model=DepsBenchResponse,
include_in_schema=False,
name=f"read_deps_mirror_{index}",
)
_register_mirror_routes()
schemas.py
from pydantic import BaseModel
class DepsBenchResponse(BaseModel):
depth: int
width: int
route_count: int
leaf_sum: int
chain_tip: int
ネストしたDI組み立て
deps.py
"""ネストした Depends を大量に組み立てる(dependency メモリ計測用)。"""
from __future__ import annotations
import inspect
from collections.abc import Callable
from typing import Any
from fastapi import Depends
CHAIN_DEPTH = 24
FANOUT_WIDTH = 8
ROUTE_COUNT = 40
def _with_depends_signature(
name: str,
param_deps: list[tuple[str, Callable[..., Any]]],
body: Callable[..., Any],
) -> Callable[..., Any]:
"""各 Depends を個別パラメータとしてシグネチャに載せ、FastAPI に認識させる。"""
parameters = [
inspect.Parameter(
param_name,
kind=inspect.Parameter.KEYWORD_ONLY,
default=Depends(dep),
)
for param_name, dep in param_deps
]
def wrapper(**kwargs: Any) -> Any:
return body(**kwargs)
wrapper.__name__ = name
wrapper.__qualname__ = name
wrapper.__signature__ = inspect.Signature(parameters) # type: ignore[attr-defined]
return wrapper
def _make_leaf(index: int) -> Callable[[], int]:
def leaf() -> int:
return index
leaf.__name__ = f"leaf_{index}"
leaf.__qualname__ = f"leaf_{index}"
return leaf
def _make_fanout_node(
level: int,
index: int,
child_deps: list[Callable[..., Any]],
) -> Callable[..., dict[str, Any]]:
param_deps = [(f"c{i}", dep) for i, dep in enumerate(child_deps)]
def body(**kwargs: Any) -> dict[str, Any]:
values = list(kwargs.values())
total = 0
for value in values:
total += value if isinstance(value, int) else int(value.get("sum", 0))
return {
"level": level,
"index": index,
"sum": total,
"width": len(values),
}
return _with_depends_signature(f"fanout_L{level}_{index}", param_deps, body)
def _make_chain_node(
level: int,
prev: Callable[..., Any],
side_deps: list[Callable[..., Any]],
) -> Callable[..., dict[str, Any]]:
param_deps = [("previous", prev), *[ (f"s{i}", dep) for i, dep in enumerate(side_deps) ]]
def body(**kwargs: Any) -> dict[str, Any]:
previous = kwargs["previous"]
sides = [kwargs[f"s{i}"] for i in range(len(side_deps))]
side_sum = 0
for side in sides:
side_sum += side if isinstance(side, int) else int(side.get("sum", 0))
prev_val = previous if isinstance(previous, int) else int(previous.get("tip", 0))
return {
"level": level,
"tip": prev_val + side_sum + level,
"side_count": len(sides),
}
return _with_depends_signature(f"chain_L{level}", param_deps, body)
def build_dependency_graph() -> tuple[Callable[..., dict[str, Any]], dict[str, int]]:
leaves = [_make_leaf(i) for i in range(FANOUT_WIDTH)]
current_layer: list[Callable[..., Any]] = list(leaves)
for level in range(1, 4):
next_layer: list[Callable[..., Any]] = []
for i in range(FANOUT_WIDTH):
children = [
current_layer[(i + offset) % len(current_layer)]
for offset in range(min(3, len(current_layer)))
]
next_layer.append(_make_fanout_node(level, i, children))
current_layer = next_layer
chain: Callable[..., Any] = current_layer[0]
for level in range(4, CHAIN_DEPTH):
sides = [
current_layer[j % len(current_layer)]
for j in range(min(FANOUT_WIDTH, len(current_layer)))
]
chain = _make_chain_node(level, chain, sides)
meta = {
"depth": CHAIN_DEPTH,
"width": FANOUT_WIDTH,
"route_count": ROUTE_COUNT,
}
return chain, meta
root_dependency, graph_meta = build_dependency_graph()
計測
まずは旧バージョン 0.139.2 で計測
バージョン確認
docker compose exec api python -c "import fastapi; print(fastapi.__version__)"
curl -s http://127.0.0.1:8000/deps
echo
アイドルメモリ
docker stats --no-stream --format "{{.Name}} {{.CPUPerc}} {{.MemUsage}}" | grep '^api-api-1 '
CPUが2%未満になってから実行
for i in $(seq 1 5); do
date -Iseconds
docker stats --no-stream --format "{{.Name}} {{.CPUPerc}} {{.MemUsage}}" | grep '^api-api-1 '
sleep 1
done | tee bench/mem_idle_0.140.0.txt
次に0.140.0で計測する
バージョン確認
アイドルメモリ
モニタリング
負荷計測(wrk)
Latency / RPS は wrk で計測(各バージョンで同一条件・複数回)。
# ウォームアップ
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" http://127.0.0.1:8000/deps
# 本計測:4スレッド・20コネクション・30秒
wrk -t4 -c20 -d30s --latency "http://127.0.0.1:8000/deps"
改善率比較(0.139.2 → 0.140.0)
計算:
- メモリ削減率 =
(旧 − 新) / 旧 × 100(大きいほど改善) - Latency / timeout 変化率 =
(新 − 旧) / 旧 × 100(負なら短縮=改善) - RPS / 総リクエスト変化率 =
(新 − 旧) / 旧 × 100(正なら改善)
サマリ表
| 指標 | 0.139.2(旧) | 0.140.0(新) | 差分 | 変化率 | 評価 |
|---|---|---|---|---|---|
| アイドル RSS | 1.581 GiB(1619 MiB) | 771.5 MiB | −847 MiB | −52.3%(使用量) | 改善(約半減) |
| Latency avg | 615.16 ms | 953.57 ms | +338.41 ms | +55.0% | 悪化 |
| Latency p50 | 614.93 ms | 953.75 ms | +338.82 ms | +55.1% | 悪化 |
| Latency p99 | 676.66 ms | 1.05 s | +約 373 ms | +55.2% | 悪化 |
| Requests/sec | 34.28 | 20.72 | −13.56 | −39.6% | 悪化 |
| 総リクエスト(30s) | 965 | 584 | −381 | −39.5% | 悪化 |
| timeout | 20 | 40 | +20 | +100% | 悪化 |
メモリ改善の内訳
| 項目 | 値 |
|---|---|
| 旧(0.139.2) | 1.581 × 1024 = 1618.9 MiB |
| 新(0.140.0) | 771.5 MiB |
| 削減量 | 1618.9 − 771.5 = 847.4 MiB |
| 削減率 | 847.4 / 1618.9 ≈ 52.3% |
| 新 / 旧 | 771.5 / 1618.9 ≈ 47.7%(旧の半分弱) |
結果
-
メモリ削減は明確
ネストDependsを大量に載せたアプリで、アイドル RSS が 1.581 GiB → 771.5 MiB(52.3% 削減)。公式 PR の狙い(dependency メモリ)と一致する。 -
今回の wrk ではスループットは上がっていない
0.140.0 の方が Latency が長く(+55%)、RPS は下がった(−39.6%)。#16049 はメモリ最適化であり、この極端な dependency グラフの解決速度まで保証するものではない。timeout も増えている。
| 検証対象 | 結果 |
|---|---|
| 0.140.0 の dependency メモリ改善 | 再現できた(アイドル RSS 52.3% 削減) |
| wrk の RPS / latency 改善 | 今回の負荷では観測されず、むしろ悪化(理由は後述) |
「メモリを食う Depends だらけの API」を抱えているなら 0.140.0 への更新は効きやすい。スループット改善を期待するなら、別指標・別負荷(例: 0.130.0 前後の巨大 JSON)で測る必要がある。
なぜ悪化したか
悪化した原因:#16049 によるトレードオフ
0.140.0 はメモリを減らすためにキャッシュを捨て、その分だけリクエスト処理が重くなった。
これまで(0.139.2)FastAPI は、Depends を解析した結果(「この関数は async か?」「必要な scope は?」など)を一度計算したら覚えておく仕組みだった(@cached_property)。速いが、覚えておくぶんメモリを食う。
0.140.0(#16049)は、この「覚えておく」をやめた。
| 0.139.2(旧) | 0.140.0(新) | |
|---|---|---|
| Depends の解析結果 | 1回計算して保持 | 毎リクエスト計算し直す |
| メモリ | 多い(保持するため) | 少ない(−52%) |
| リクエストの重さ | 軽い(計算済み) | 重い(毎回計算) |
つまり 「メモリ ↔ 速度」のトレードオフ。
なぜ今回はこんなに遅くなったのか
「毎回計算し直す」コストは、Depends のネストが深いほど膨らむ。今回のベンチはそこを極端にした設定:
-
CHAIN_DEPTH=24(24段のネスト)・FANOUT_WIDTH=8・ルート40本 - → 1リクエストで数千ノードを毎回解析し直す = Latency +55%
つまり「捨てたキャッシュが一番効いていた形」をわざと作ったベンチ。改善の悪い面だけが最大化されて出た数字であり、一般的な API には当てはまらない極端な例。
補足:RPS と Latency は「別々の悪化」ではない
今回は接続数を固定した負荷(-c20)なので、RPS ≒ 接続数 ÷ Latency の関係で決まる。
- 0.139.2: 20 ÷ 0.615s ≒ 34(実測 34.3)
- 0.140.0: 20 ÷ 0.953s ≒ 21(実測 20.7)
RPS 低下と Latency 増加は同じ事実の裏表。「1リクエストが重くなった」の一点に集約される(uvicorn 1プロセス=実質1コアで捌いているのも効いている)。
実運用で起こる可能性があるか
ほぼ起こらない。
- 普通の API は Depends のネストが浅い(数段程度)。毎回計算し直しても誤差レベルで、速度差は体感できない。
- 今回のような24段ネストは現実では作らない。
- 逆に メモリ削減(−52%)は多くのアプリで効く。特に Depends を多用する大きめのアプリほど恩恵が大きい。
→ ほとんどの環境で 0.140.0 へのアップデートはプラス。速度が落ちて見えるのは、今回のような極端なネストを組んだ特殊ケースだけ。
まとめ
- 遅くなった原因は #16049 のメモリ↔速度トレードオフ(キャッシュ廃止でメモリ半減、代わりに毎回計算で重くなった)。
- 今回の数字は 深いネスト Depends を意図的に作ったベンチマークテストなので大きく出ただけ。RPS と Latency の悪化は同じ現象。
- 実運用ではほぼ問題にならず、メモリ削減の恩恵の方が大きい。
今回の検証用リポジトリ








