FastAPIで非同期処理を扱うときは、async/await、BackgroundTasks、WebSocketを同じ仕組みとして考えないことが出発点です。
async/awaitは待ち時間を別の処理に譲るための構文で、BackgroundTasksはレスポンス送信後に小さな処理を続ける機能です。
一方、WebSocketはクライアントとサーバーの接続を保ち、双方向にメッセージを送る通信方式です。
FastAPIの特徴とAPI設計の基本を理解している読者に向けて、三つの役割と実運用での境界をコード例で整理します。
三つの仕組みを使い分ける
選択基準は「いつ処理するか」と「クライアントとの接続を保つか」です。
| 仕組み | 向いている処理 | 注意点 |
|---|---|---|
async/await |
データベースや外部APIなど、待ち時間がある入出力処理 | CPUを使い続ける計算が自動で速くなるわけではない |
BackgroundTasks |
レスポンス後に同じアプリ内で行う小さな処理 | 永続的なジョブキューや再試行の仕組みではない |
| WebSocket | チャット、進捗通知、ライブ更新などの双方向通信 | 切断、再接続、認証、接続数を含む運用設計が必要 |
asyncとawaitが解決する待ち時間
非同期処理とは、ネットワークやファイルなどの完了を待つあいだに、プログラムが別の処理を進められる実行方法です。
async defで定義した関数はコルーチンとなり、awaitに到達すると、待機中にほかの処理へ実行を譲れます。
import asyncio
async def fetch_summary(name: str, delay: int) -> str:
await asyncio.sleep(delay)
return f"{name} 完了"
async def main() -> None:
results = await asyncio.gather(
fetch_summary("task-a", 1),
fetch_summary("task-b", 2),
)
print(results)
asyncio.run(main())
この例では、二つの待機を一つずつ終わらせてから次へ進むのではなく、asyncio.gather()が両方のコルーチンを並行して進めます。
ここでの並行処理は、二つの計算を複数のCPUで同時に実行する並列処理とは別です。
画像変換や大規模な数値計算のようにCPUを長く使う処理は、イベントループを占有しないように別プロセスやジョブ基盤へ分けます。
パス操作関数をdefとasync defのどちらで書くか
awaitできるデータベースやHTTPクライアントを呼ぶ場合は、パス操作関数をasync defで定義します。- 同期I/Oしか提供しないライブラリを使う場合は、パス操作関数を通常の
defで定義する選択肢があります。 async defの中で長い同期処理を直接実行すると、その処理が終わるまでイベントループを塞ぐため避けます。
BackgroundTasksでレスポンス後の処理を分ける
BackgroundTasksは、レスポンスをクライアントへ送った後で、同じFastAPIアプリ内の関数を実行する機能です。
監査ログへの追記や小規模な通知など、クライアントが完了を待つ必要のない短い処理に向きます。
from fastapi import BackgroundTasks, FastAPI, status
app = FastAPI()
def write_audit_log(task_id: str) -> None:
with open("task.log", "a", encoding="utf-8") as log_file:
print(task_id, file=log_file)
@app.post("/process/{task_id}", status_code=status.HTTP_202_ACCEPTED)
async def process_data(
task_id: str,
background_tasks: BackgroundTasks,
) -> dict[str, str]:
background_tasks.add_task(write_audit_log, task_id)
return {"status": "accepted"}
このAPIが返すacceptedは、処理を受け付けたことを示します。
バックグラウンド処理の成功まで保証する応答ではないため、完了確認が必要な業務では状態を保存し、別のAPIから確認できる設計が必要です。
失敗時の扱いとタスクキューとの境界
BackgroundTasksはアプリ内で実行されるため、処理中にプロセスが停止すれば完了しない可能性があります。
複数のタスクは追加した順に実行され、途中のタスクが例外を出すと後続タスクは実行されません。
例外を記録するログと監視を用意し、応答済みのリクエストから失敗を追跡できるようにします。
長い計算、大量の処理、再試行、複数サーバーでの実行が必要なら、FastAPIとCelery、Redisでジョブ基盤を分ける方法を検討します。
WebSocketで双方向通信を続ける
WebSocketは、一度確立した接続を保ったまま、クライアントとサーバーのどちらからもメッセージを送れる通信方式です。
HTTPリクエストを繰り返さずに更新を届けられるため、チャットや処理状況の通知に使えます。
切断を処理するエコーサーバー
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
await websocket.accept()
try:
while True:
message = await websocket.receive_text()
await websocket.send_text(f"受信: {message}")
except WebSocketDisconnect:
# 接続一覧を管理する場合は、ここで削除する
pass
accept()で接続を受け入れ、receive_text()とsend_text()で受信と送信を繰り返します。
クライアントが接続を閉じるとWebSocketDisconnectが発生するため、接続一覧や関連リソースの後始末をこのタイミングで行います。
ブラウザから接続する最小例
<form id="chat-form">
<label for="message">メッセージ</label>
<input id="message" autocomplete="off" required>
<button type="submit">送信</button>
</form>
<div id="log" role="log" aria-live="polite"></div>
<script>
const socket = new WebSocket("ws://127.0.0.1:8000/ws");
const form = document.getElementById("chat-form");
const input = document.getElementById("message");
const log = document.getElementById("log");
socket.addEventListener("message", (event) => {
const line = document.createElement("p");
line.textContent = event.data;
log.appendChild(line);
});
form.addEventListener("submit", (event) => {
event.preventDefault();
if (socket.readyState !== WebSocket.OPEN) return;
socket.send(input.value);
input.value = "";
});
</script>
入力欄にはラベルを関連付け、受信ログにはrole="log"とaria-live="polite"を指定しています。
これにより、画面へ追加されたメッセージを支援技術が穏やかに通知できます。
本番環境ではHTTPSに対応するwss://を使い、接続前の認証と権限確認、切断後の再接続方針も実装します。
複数クライアントへの配信やスケールアウトまで進める場合は、FastAPIとWebSocketの接続管理パターンも参照してください。
本番運用で確認する項目
サンプルが動いても、失敗時の挙動を決めなければ運用できません。
- 接続制御:同時接続数とメッセージサイズの上限を負荷試験から決めます。Uvicornの
--wsはWebSocketプロトコル実装を選ぶ設定で、接続数を制限する設定ではありません。 - 切断と再接続:サーバーは
WebSocketDisconnectで後始末を行い、クライアントは再接続の間隔と上限を決めます。 - 認証と認可:接続時に利用者を確認し、接続後も送受信できるデータの範囲を制限します。
- バックグラウンド処理の可観測性:成功と失敗、処理時間を記録し、応答後に発生した問題を追跡できるようにします。
- 複数ワーカー:プロセス内の接続一覧は別ワーカーと共有されません。複数プロセスから同じメッセージを配信する場合は、共有できるメッセージ基盤を用意します。
Uvicornでサーバー全体の同時接続またはタスク数を制限する設定は--limit-concurrencyです。
ただし、適切な値はアプリの処理内容、メモリ使用量、ワーカー数で変わるため、固定値を写すのではなく負荷試験から決めます。
実装を選ぶためのチェックリスト
- 待ち時間の中心がデータベースや外部APIなら、対応ライブラリと
async/awaitの組み合わせを確認する。 - レスポンス後の短い処理なら、失敗を追跡できる状態で
BackgroundTasksを使う。 - 再試行や永続化が必要なら、APIプロセスからジョブキューへ処理を渡す。
- 双方向のライブ更新が必要なら、WebSocketの切断、再接続、認証を先に設計する。
- 公開前に、通常時だけでなくプロセス停止、通信断、過負荷時の挙動もテストする。
まとめ
async/awaitは入出力待ちを効率よく扱い、BackgroundTasksはレスポンス後の小さな処理を同じアプリ内で実行します。
WebSocketは双方向通信を続ける仕組みであり、バックグラウンド処理の代わりではありません。
処理の完了保証、再試行、切断、認証、監視まで要件に含めると、サンプルコードを本番で運用できる構成へ進められます。
この記事に関連する株式会社greedenの取り組み
非同期APIは、コード例を動かすだけでなく、処理の失敗、再接続、監視まで含めて設計してこそ運用できます。株式会社greedenは、要件整理から実装、テスト、保守まで一気通貫でWebシステム開発を支援します。
