Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】My FastAPI Dependency Ran Once When I Needed It Twice

2026年10月15日に「Dev.to」が公開したITニュース「My FastAPI Dependency Ran Once When I Needed It Twice」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

FastAPIでは、依存関係はデフォルトでリクエストごとにキャッシュされ、複数回指定しても一度しか実行されない。これはパフォーマンス向上に役立つが、ログ記録など副作用を伴う処理では想定外の動きとなり、バグの原因となる場合がある。毎回実行させるには`use_cache=False`を指定する。

ITニュース解説

FastAPIは、Pythonを使用してWeb APIを開発するためのフレームワークであり、その特徴の一つに「依存性注入」という仕組みがある。これは、APIエンドポイントが動作するために必要な部品(依存関係)を、FastAPIが自動的に提供する機能だ。たとえば、データベースへの接続情報や、ユーザー認証のための処理などを依存関係として定義しておくと、FastAPIが適切にそれらを呼び出し、API関数に渡してくれる。これにより、コードの再利用性が高まり、複雑な処理も整理しやすくなる利点がある。

今回話題になっているのは、この依存性注入機能で起こった予期せぬ挙動に関する話だ。あるエンジニアが、FastAPIを使ってAPIエンドポイントに2種類のレート制限チェック(ユーザーアカウントに基づくものと、IPアドレスに基づくもの)を実装しようとした。彼は、これらのチェックを実行するrate_limit_checkという依存関係関数を作成し、APIエンドポイントの関数内でこのrate_limit_checkをcheck_a: dict = Depends(rate_limit_check)とcheck_b: dict = Depends(rate_limit_check)のように、2回宣言した。彼の意図は、rate_limit_checkが2回実行され、それぞれのアカウントとIPの制限が独立して機能することだった。開発段階やコードレビューでは特に問題は見当たらず、システムは本番環境にデプロイされた。

しかし、本番環境での稼働中に、IPアドレスによるレート制限が想定通りに動作していないことが判明した。原因を探るためログを詳しく調べた結果、驚くべきことに、2回宣言したはずのrate_limit_check関数が、実際には1回しか呼び出されていないことが判明したのだ。check_aとcheck_bの両方には、1回の実行で得られた全く同じ結果が渡されていた。つまり、2つの異なるチェックとして機能するはずが、実際には1つのチェックが2つの名前で扱われている状態だった。

この現象の背景には、FastAPIの依存性注入機能に組み込まれている「リクエストごとのキャッシュ」というデフォルトの仕組みがあった。FastAPIのドキュメントにも記載されているこの機能は、もし一つのAPIリクエスト処理中に同じ依存関係が複数回宣言された場合、その依存関係関数を最初に1回だけ呼び出す。そして、その呼び出しで得られた結果を「キャッシュ」として保存し、同じリクエスト内でその依存関係を必要とする他のすべての場所には、このキャッシュされた同じ値を渡す、というものだ。

このキャッシュ機能は、通常はFastAPIの非常に優れた、かつ重要な特徴だ。これはAPIのパフォーマンスを大きく向上させるために設計されている。たとえば、多くのAPIエンドポイントが、データベースから現在のユーザー情報を取得するget_current_userのような共通の依存関係を利用している場合を考えてみよう。キャッシュ機能がなければ、一つのリクエスト内でget_current_userが5回呼び出されれば、データベースへのアクセスも5回発生してしまう。しかし、FastAPIのキャッシュ機能があれば、get_current_userは1回だけ実行され、その結果がキャッシュされるため、データベースへのアクセスも1回で済む。このように、共通の処理を効率的に一度だけ実行することで、APIの応答速度を速め、システムへの負荷を軽減する効果があるのだ。

しかし、今回のレート制限チェックのケースでは、このキャッシュ機能が「サイレントバグ」(気づかれにくいバグ)として現れた。レート制限チェックは、単に情報を読み取るだけでなく、「ヒットを記録する」という、システムの外部状態を変化させる「副作用」を持つ。FastAPIは、エンジニアがrate_limit_checkを2回宣言したとしても、パフォーマンス最適化のため、同じ依存関係として1回だけ実行し、その結果をキャッシュした。結果として、本来2回発生するべき「ヒットの記録」が1回しか行われず、片方のレート制限が正しく適用されなかったのだ。

このようなバグが見つけにくいのは、ほとんどの依存関係が「純粋」であるためだ。純粋な依存関係とは、同じ入力に対して常に同じ出力を返し、外部の状態を一切変更しないものを指す。例えば、現在のユーザー情報を読み取るだけ、リクエストパラメータを解析するだけ、設定ファイルを読み込むだけ、といった依存関係は純粋だ。これらは1回呼び出されても2回呼び出されても結果に違いはなく、システムに影響も与えないため、キャッシュされても問題なく動作し、エンジニアはそのキャッシュの存在に気づかないことが多い。しかし、レート制限のように「ヒットを記録する」「新しいトークンを生成する」「カウンターを増やす」「可変な状態を再読み込みする」といった副作用を持つ依存関係の場合、1回しか実行されないと期待通りの結果にはならない。また、開発環境でのテストは、多くの場合、一度に一つの処理パスしか検証しないため、依存関係が複数回呼び出される状況をシミュレートしない限り、この種のバグは表面化しにくい傾向がある。

この問題の解決策は、原因が理解できれば非常にシンプルだった。FastAPIには、依存関係のキャッシュを無効にするためのuse_cache=Falseというオプションが用意されている。これをDepends()の中に含めることで、その依存関係はリクエスト内で何回宣言されても、毎回新しく呼び出されるようになる。

具体的には、check_a: Annotated[dict, Depends(rate_limit_check, use_cache=False)]のように記述する。このuse_cache=Falseを設定することで、rate_limit_checkは2回実行されるようになり、両方のレート制限が正しく機能し、ヒットも2回記録されるようになった。FastAPIのドキュメントでは、このオプションは「同じリクエスト内で依存関係を毎回(複数回になる可能性もある)呼び出す必要があることがわかっている、高度なシナリオ」のためにあると説明されており、まさに副作用を持つ依存関係に当てはまるケースだ。

この経験から得られる重要な教訓は、FastAPIの依存関係がデフォルトでリクエストごとにキャッシュされることを常に意識し、自分の作成する依存関係が「純粋」なのか、それとも「副作用を持つ」のかを明確に区別することだ。ユーザー情報の取得や設定の読み込みのような純粋な依存関係は、キャッシュによってパフォーマンスが向上するため、デフォルトのキャッシュ挙動をそのまま利用すべきだ。しかし、システムの状態を変更する(例: データベースに書き込む、外部サービスにデータを送信する、カウンターを増やす、ログを記録する)あるいは毎回新しい値を生成する必要がある依存関係(例: 新しいトークンを生成する、最新の時刻を読み込む)の場合は、use_cache=Falseを設定して、意図した回数だけ実行されるようにする必要がある。

開発時には、特にこのようなフレームワークのデフォルト設定による暗黙的な挙動には注意が必要だ。単一のテストでは隠れてしまうような問題は、本番環境でしか現れないことがある。そのため、副作用を持つ依存関係を扱う場合は、それがリクエスト内で複数回呼び出される可能性があるかどうかを考慮し、もし1回しか実行されない場合に問題が生じるならuse_cache=Falseを適用することが重要だ。また、そのような「チェックと記録」のような副作用のある処理は、依存関係としてモデル化するのではなく、APIエンドポイント内で明示的に呼び出すか、あるいは最初からuse_cache=Falseを意図して依存関係として定義することで、その挙動をより明確にコードに反映させることが推奨される。

FastAPIアプリを開発する際には、自分が定義した依存関係の中で、何かを記録したり、増やしたり、生成したり、送信したりするような副作用を持つものがないかを確認し、それがリクエスト内で複数回呼び出される可能性があるかどうか、そしてもし1回しか実行されない場合に問題がないかを自問自答することが、同様のバグを防ぐための重要なステップとなるだろう。

関連コンテンツ

関連IT用語