green snake
Photo by Pixabay on Pexels.com
目次

FastAPIパフォーマンスチューニング&キャッシュ戦略入門:遅いAPIから「軽くて速いAPI」に育てる実践レシピ


最初にざっくり全体像(要約)

  • FastAPIのパフォーマンスを落としている正体の多くは、フレームワーク自体ではなく「DBクエリ」「N+1問題」「インデックス不足」「キャッシュ不在」といった設計まわりです。
  • まずは非同期エンドポイントの正しい使い方と、SQLAlchemy 2系の接続プール・クエリ最適化を押さえることで、無駄な待ち時間を減らします。
  • そのうえで、HTTPキャッシュヘッダ(Cache-ControlETag)・fastapi-cache2・Redisなどを組み合わせ、レスポンスや関数結果をキャッシュすることで、重い処理を繰り返さずに済むようにします。
  • Uvicornのワーカー数・JSONレスポンスの選択・ミドルウェアの見直しなど、「FastAPIならではの細かな最適化」を積み重ねることで、全体として軽くて扱いやすいAPIになります。
  • 最後に、「どこから手を付ければよいか」のロードマップを示しつつ、学習者・小規模チーム・SaaS開発チームそれぞれにとって、どんなインパクトがあるのかを整理します。

誰が読んで得をするか(具体的な読者像)

1. 個人開発・学習者さん

  • HerokuやVPS、小さなクラウド環境でFastAPIアプリを動かしている。
  • ユーザーやデータが少し増えてきて、「たまにレスポンスがもっさりする」感覚が出てきた。
  • 「asyncにしたから速いはず…」と思っているけれど、本当にできているか少し不安。

この方には、「まずどこを疑えばよいか」「どんな順番で改善すればよいか」という観点で、無理なく実践できるステップをお伝えします。

2. 小規模チームのバックエンドエンジニアさん

  • 3〜5人くらいでFastAPI+SQLAlchemyベースのAPIを開発しており、機能追加と同時に「遅さ」もじわじわ増えている。
  • N+1クエリやインデックス不足をなんとかしたいが、どこから触ればよいか迷っている。
  • キャッシュを入れたいが、「HTTPキャッシュ」「Redis」「fastapi-cache2」など選択肢が多くて悩んでいる。

この方には、DBまわりとキャッシュ戦略を「3層構造」で整理しながら、チーム内で共有しやすいベースラインを提案します。

3. SaaS開発チーム・スタートアップのみなさま

  • すでにユーザー数やトラフィックがそれなりにあって、ピークタイムのレイテンシやスループットが事業に直結している。
  • RedisやCDNなども使い始めているが、「どこまでAPI側でキャッシュを意識するべきか」の線引きが悩ましい。
  • 将来的にマルチインスタンスやKubernetesでのスケールを見据えつつ、「今から外さない」設計をしたい。

この方には、「ボトルネックの見つけ方」「SQLAlchemy&キャッシュの実践的なパターン」「HTTPキャッシュを含めた多層キャッシュ戦略」の整理がお役に立てると思います。


アクセシビリティ評価(読みやすさ・理解のしやすさ)

  • 情報の構造
    • まず「ボトルネックの考え方」を整理し、そのあとに「DB最適化 → キャッシュ戦略 → FastAPI固有のチューニング → ロードマップ」という順で深掘りします。
  • 用語の扱い
    • N+1クエリ・接続プール・HTTPキャッシュヘッダなど、専門用語は初出時に短く補足を入れています。
  • コード例
    • 1ブロックを短めに、コメントも最低限にして、視線が迷わないようにしています。
  • 想定読者
    • FastAPIチュートリアルを一通り試した中級者を主な想定としつつ、章ごとに独立して読んでも雰囲気がつかめるように書いています。

全体として、技術記事としての読みやすさ・理解しやすさの面でWCAGのAA程度を意識した構成にしています。


1. パフォーマンス問題の正体:まず「どこが遅いか」を疑う

最初に強調しておきたいのは、「FastAPIだから遅い」というケースは思った以上に少ない、ということです。

よくあるボトルネックは次のようなものです。

  • DBまわり
    • N+1クエリ(ループの中で毎回SELECTしてしまう)
    • インデックスがない or 足りない
    • 不要なカラムやテーブルを全部引いている
  • ネットワークまわり
    • 外部APIを連続で呼んでいる
    • タイムアウトや再試行戦略が甘く、待ち時間が雪だるま式に増える
  • アプリケーション層
    • キャッシュがなく、同じ重い処理を何度も繰り返している
    • CPUに重い処理(画像加工・大きなJSONの整形など)を1ワーカーで抱え込んでいる

FastAPI固有のチューニング(JSONレスポンスのクラス変更やワーカー数の調整など)は、上記のような「根本原因」をある程度潰したあとで効いてきます。


2. 非同期エンドポイントとSQLAlchemyの基本チューニング

2.1 async/awaitの「守るべきライン」

FastAPIは、同期関数(def)と非同期関数(async def)両方のエンドポイントをサポートします。
ただし「なんでもasyncにすれば速くなる」というわけではありません。

  • I/Oバウンド(DB・外部API・ファイルアクセスなど)の処理は、非同期化の恩恵が大きい
  • CPUバウンド(画像変換・重い計算など)は、GILの影響もあってスレッドやasyncでは限界があり、プロセス分離(Celeryなど)のほうが有効

非同期エンドポイントを書くときは、「中で呼んでいるライブラリも非同期対応か」を必ず確認しておきましょう。
同期のSQLAlchemyセッションをasync defから呼びまくる、という構成だと、思ったほどスループットは上がりません。

2.2 SQLAlchemy 2系+接続プール

DB接続を毎回新しく開いていると、それだけで大きなオーバーヘッドになります。
SQLAlchemyでは create_engine() の引数で接続プールを設定できます。

# app/infra/db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql+psycopg://user:pass@localhost:5432/app"

engine = create_engine(
    DATABASE_URL,
    pool_size=20,      # 同時接続のベースライン
    max_overflow=0,    # これ以上は増やさない(環境に応じて調整)
)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)

FastAPI側では、依存関数でセッションを使い回します。

# app/deps/db.py
from app.infra.db import SessionLocal

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

この「接続プール+依存関数」の組み合わせは、ほとんどのFastAPI+SQLAlchemyアプリで使える基本パターンです。

2.3 N+1クエリとインデックス

N+1クエリ問題は、ORMを使っていると本当に気づきづらい落とし穴です。

  • 例:ユーザー一覧をループしつつ、そのたびに .posts を参照して追加のSELECTが走る
  • 対策:selectinload()joinedload() を使った eager load、JOIN を使ったクエリに書き換える

また、WHERE句やJOINに使っているカラムにインデックスがないと、テーブルが大きくなるほどクエリが急激に遅くなります。
DB側の実行計画(EXPLAIN)を見ながら、必要なインデックスを用意しましょう。


3. キャッシュの基本設計:何をどこに置くか

次に、キャッシュ戦略を考えます。

3.1 キャッシュの層をざっくり3つに分ける

キャッシュは「どこに置くか」で大きく3つに分けられます。

  1. クライアント・CDN・プロキシ側(HTTPキャッシュヘッダ)

    • ブラウザやCDNがレスポンスを覚えておいてくれる
    • Cache-ControlETag などで制御
  2. アプリケーション側(メモリ/Redisなど)

    • FastAPIアプリから見て「近い」場所に置く
    • 関数の結果やクエリ結果をキャッシュ
  3. DB側

    • マテリアライズドビューや専用の集計テーブルで、あらかじめ重い集計の結果を保存しておく

この記事では、特に 1 と 2 を中心に見ていきます。

3.2 何をキャッシュしてはいけないか

  • 個人情報やセッション情報など、ユーザーごとに異なるものを「共有キャッシュ」に載せるのは危険
  • Cache-Control: private や、ユーザー固有のキー(user_id)を含むアプリ側キャッシュを活用し、「誰が見てもいいデータ」とそうでないものを分ける

4. FastAPIでHTTPキャッシュヘッダを扱う

まずはHTTPキャッシュからです。
これは、アプリ側の実装は薄く済むのに効果が高いことが多く、「最初の一手」としておすすめです。

4.1 Cache-Control を付ける

例えば「ランキング情報を60秒間キャッシュしてほしい」エンドポイントを考えます。

# app/api/ranking.py
from fastapi import APIRouter, Response

router = APIRouter(prefix="/ranking", tags=["ranking"])

@router.get("")
def get_ranking(response: Response):
    # 本当はDBなどから集計
    data = {"items": ["A", "B", "C"]}

    # 60秒間キャッシュOK(中間プロキシも含む)
    response.headers["Cache-Control"] = "public, max-age=60"
    return data

これだけで、ブラウザやCDNがレスポンスを一定時間再利用してくれるようになります。

4.2 ETag と条件付きリクエスト

「データが変わっていなければ304(Not Modified)で返す」というパターンもよく使われます。

import hashlib
import json
from fastapi import Request, Response, HTTPException

@router.get("/popular")
def get_popular_items(request: Request, response: Response):
    data = {"items": ["A", "B", "C"]}
    body = json.dumps(data, sort_keys=True).encode("utf-8")
    etag = hashlib.sha1(body).hexdigest()

    # クライアントからIf-None-Matchが送られてきた場合
    inm = request.headers.get("if-none-match")
    if inm == etag:
        # 内容に変化がないので304だけ返す
        response.status_code = 304
        return Response(status_code=304)

    response.headers["ETag"] = etag
    response.headers["Cache-Control"] = "public, max-age=120"
    return data

実運用では、ETag をDBの更新バージョンや更新日時から計算することも多いです。


5. fastapi-cache2+Redisでアプリ側キャッシュを入れる

HTTPキャッシュだけでは足りない場合、アプリケーション側でRedisなどのキャッシュを用意します。
ここでは fastapi-cache2 というライブラリを使った例を紹介します。

5.1 インストール

pip install fastapi-cache2 redis

Redis自体は、先ほどと同じようにDockerで立ち上げておきます。

docker run -d --name redis -p 6379:6379 redis:7

5.2 初期化コード

# app/core/cache.py
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
import redis.asyncio as redis

async def init_cache():
    client = redis.from_url("redis://localhost:6379/0", encoding="utf8", decode_responses=True)
    FastAPICache.init(
        backend=RedisBackend(client),
        prefix="fastapi-cache:",
    )
# app/main.py
from fastapi import FastAPI
from app.core.cache import init_cache

app = FastAPI(title="FastAPI Cache Example")

@app.on_event("startup")
async def on_startup():
    await init_cache()

5.3 エンドポイント(または関数)をキャッシュする

fastapi-cache2@cache() デコレータを使うと、関数の戻り値を簡単にキャッシュできます。

from fastapi import APIRouter
from fastapi_cache.decorator import cache

router = APIRouter(prefix="/articles", tags=["articles"])

@router.get("")
@cache(expire=60)   # 60秒間キャッシュ
async def list_articles():
    # 本当はDBから重いクエリを投げていると仮定
    return [{"id": 1, "title": "Hello"}, {"id": 2, "title": "World"}]

初回呼び出し時に結果がRedisに保存され、次の60秒間はRedisから高速に返されます。

5.4 ユーザーごとのキャッシュキーを付けたいとき

@cache()namespacekey_builder を指定することで、キャッシュキーのカスタマイズも可能です。ユーザーIDごとのキャッシュを分けたいときに便利です。

from fastapi import Depends
from fastapi_cache.decorator import cache
from app.deps.auth import get_current_user

def user_cache_key_builder(func, *args, **kwargs):
    request = kwargs.get("request")
    user = kwargs.get("current_user")
    return f"user:{user.id}:path:{request.url.path}"

@router.get("/me")
@cache(expire=30, key_builder=user_cache_key_builder)
async def get_my_dashboard(
    request: Request,
    current_user=Depends(get_current_user),
):
    # ユーザー別の重い集計結果
    ...

実際には、引数の取り扱いなどを慎重に設計する必要がありますが、考え方としてはこのようなイメージです。


6. 多層キャッシュ戦略と「期限」「無効化」の考え方

キャッシュは入れるだけでなく、「いつまで有効にするか」「どうやって無効化するか」が非常に大事です。

6.1 有効期限の決め方

  • データが頻繁に変わるが「多少の遅れは許容できる」
    • 秒〜分単位の短い有効期限(例:ランキング、人気記事など)
  • あまり変わらないデータ
    • 分〜時間単位の長めの有効期限(例:マスターデータ)
  • 絶対に古くてはいけないデータ
    • キャッシュしないか、no-cache(再検証必須)でHTTPキャッシュを制御

「すべてをキャッシュする」のではなく、「多少古くても良いもの」から優先して対象にしていくのが現実的です。

6.2 キャッシュ無効化のパターン

  • 時間で自然に失効させる(expiremax-age に任せる)
  • 更新イベントでピンポイントに削除する(記事が更新されたら、その記事詳細のキャッシュだけ削除)
  • バージョン付きキーを使う(v1:ranking → 更新タイミングでv2:rankingに切り替える)

特に、Redisで複雑なキャッシュを扱っている場合は「キーの命名規則」をチームで共有しておくと、あとから見返しても分かりやすくなります。


7. FastAPI固有のチューニングポイント

ここまでで、設計寄りの話をしてきました。
次は「FastAPIならでは」の細かな調整ポイントをいくつか見ていきます。

7.1 Uvicornのワーカー数

Uvicornの起動オプション --workers で、マルチプロセス実行ができます。

uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4
  • CPUコア数をベースに、コア数〜2×コア数 あたりから試す
  • 重い処理が多い場合は、ワーカー数だけでなく「処理自体をCeleryなどに任せる」ことも検討

7.2 JSONレスポンスクラスの選択

大きなJSONを返す場合、レスポンスクラスを変えることで多少の差が出るケースもあります。

from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)

orjson は高速なJSONライブラリとして知られており、CPU負荷が高い場合には一つの選択肢になります。

7.3 ミドルウェアの数と順序

リクエストごとに通過するミドルウェアが多すぎると、それだけでオーバーヘッドになります。

  • 本当に必要なミドルウェアだけに絞る
  • 重い処理を行うミドルウェアは、なるべく特定のルートやサブアプリに限定する

8. 計測と検証:どれくらい速くなったかを「数字」で見る

パフォーマンスチューニングは、「やったつもり」にならないためにも計測が大切です。

  • ローカル・ステージング環境で
    • ab, hey, wrk などの負荷ツールで一定時間叩いてみる
    • P95/P99レイテンシ・エラー率・RPSの変化を見る
  • 本番では
    • Prometheus+Grafanaなどでレイテンシやスループットを可視化
    • 「キャッシュヒット率」や「DBクエリ数」もメトリクス化できると理想的

「コードを書いた → 軽くなった気がする」ではなく、「キャッシュ導入前後でP95が○ms→△msに下がった」といった具体的な数字で確認するようにしましょう。


9. 読者別に見たインパクトと、どこから始めるか

9.1 個人開発・学習者さんへ

  • まずは**HTTPキャッシュヘッダ(Cache-Control)**と、SQLAlchemyの接続プールから始めるのがおすすめです。
  • 次に、読み取り専用の重いAPIに対してfastapi-cache2を試し、「キャッシュがどれだけ楽をさせてくれるか」を体感してみてください。
  • 数人〜数十人規模のサービスでも、レスポンスの軽さや「余裕」がかなり変わってくるはずです。

9.2 小規模チームのエンジニアさんへ

  • チームで「パフォーマンス改善の方針」を共有するときは、
    1. N+1クエリ・インデックス不足の解消
    2. 接続プールの適切な設定
    3. キャッシュ戦略(HTTP+Redis)の導入
      の3段階で整理すると話がしやすくなります。
  • 最初に「ボトルネックはどこか?」をメトリクスやログから特定し、効果の大きいところからキャッシュや最適化を入れていきましょう。

9.3 SaaS開発チーム・スタートアップのみなさまへ

  • 多層キャッシュ戦略(HTTP・Redis・DB)を前提におき、「どの層にどのデータを置くか」をドキュメント化しておくと、チーム内の認識が揃いやすくなります。
  • RedisやCDNを入れる前に、「DBクエリの質」「スキーマ設計」「N+1解消」といった根本的な部分を見直すだけで、10倍以上改善するケースも珍しくありません。
  • そのうえで、fastapi-cache2 や独自のキャッシュ層を組み合わせていけば、無理なくスケールしていけると思います。

10. 導入ロードマップ(少しずつ進めるために)

最後に、「これから実際にパフォーマンス改善とキャッシュ導入を進めるときの道筋」を段階的にまとめます。

  1. 現状の計測

    • 代表的なAPIのレイテンシとRPSを測る
    • DBクエリ数や外部API呼び出し数をログに出してみる
  2. DB・クエリまわりの見直し

    • N+1クエリやインデックス不足を解消
    • 接続プールの設定を適切な値に調整
  3. HTTPキャッシュヘッダの導入

    • 読み取り専用で「多少古くてもよいAPI」からCache-Controlを付けてみる
    • 場合によってはETagLast-Modifiedも検討
  4. アプリケーション側キャッシュの導入

    • Redis+fastapi-cache2で、特定のエンドポイントや関数の結果をキャッシュ
    • 有効期限・キー設計・無効化戦略を整理
  5. FastAPI固有のチューニング

    • Uvicornワーカー数・レスポンスクラス・ミドルウェア構成を見直す
    • ボトルネックがCPUなら、Celeryなど別プロセスへのオフロードも検討
  6. 本番での継続的な観測と調整

    • メトリクスとログを見ながら、改善→計測→改善…のサイクルを回す
    • プロダクトの成長に合わせて、キャッシュ戦略やDB設計もアップデートしていく

参考リンク(さらに深く学びたい方へ)

※内容やバージョンは執筆時点の情報です。最新情報は各サイトでご確認ください。


おわりに

パフォーマンスチューニングやキャッシュ戦略というと、どうしても「難しそう」「一度決めたら変えづらそう」というイメージがあるかもしれません。
でも、実際には「よく使うAPIから少しずつ」「多少の古さが許されるデータから」手を付けていくだけでも、体感は大きく変わってきます。

  • まずは、ボトルネックを見つけること
  • 次に、DB・クエリまわりを整えること
  • そして、HTTPキャッシュやRedisキャッシュを組み合わせて、重い処理を何度も繰り返さないようにすること

この3つを少しずつ進めていくだけで、FastAPIアプリはぐっと「軽くて、扱いやすい相棒」に近づいていきます。

どうぞ、ご自分やチームのペースに合わせて、一つずつ試してみてくださいね。
わたしも、あなたのAPIが気持ちよく動き続けてくれることを、そっと応援しています。


投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)