FastAPIのJSONレスポンスに型を書くだけでレスポンスが速くなるらしい
2/25追記
ベンチマークの測り方が間違っていました。
response_modelも高速化の対象になるそうです。
間違った情報を掲載してしまい大変申し訳ありません🙇♂️
検証終わり次第ベンチマーク部分を更新します。
追記:ベンチマーク更新しました。明らかに倍以上のパフォーマンス出てます。
はじめに
2月23日にFastAPI 0.131.0がリリースされました。
中でも注目されたのが以下のツイートです。
要約すると、「戻り値に型だけ指定することで、レスポンスの速さが最大2倍になる」とのことです。
え?すごくないすか?
class User(BaseModel):
id: int
name: str
email: str
is_active: bool
@app.get("/user")
def get_users():
...
これを...
class User(BaseModel):
id: int
name: str
email: str
is_active: bool
@app.get("/user")
def get_users() -> list[User]:
...
または
class User(BaseModel):
id: int
name: str
email: str
is_active: bool
@app.get("/user", response_model=list[User])
def get_users():
...
こうするだけでパフォーマンスが上がるらしいです。
なんで早くなったん?
以下のPRを読み解きます。
✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model
訳:Pydantic の戻り値型またはレスポンスモデルがある場合、Pydantic (Rust 内) を使用して JSON レスポンスをシリアル化します
めっちゃシンプルな説明ですね。
PydanticはPythonで使われているバリデーションライブラリで、主にAPI周りの入出力の型チェックに使われています。
PydanticでRust?と思われますが、実は現行のPydantic v2はRustで書かれており、v1に比べて倍以上の処理速度を実現しているとのこと。
早くなっただけでなく、v2ではデフォルトでjsonシリアライズに対応するようになり、Pydanticのみでシリアライズが可能になったそうです。
つまりRust側でjsonシリアライズすることで高速になったそうです。
もうちょい補足
大きく変わったのはPRのこの部分。
dump_jsonが有効の場合、serialize_jsonを使ってjsonシリアライズをします。これがRustでシリアライズする関数になります。
一方でdump_jsonが無効の場合は従来通りserializeが使われます。
dump_pythonはpythonオブジェクトを返却するため、それをjsonに変換する手間がありました。
一応変換自体はRustでしているのですが、そこにPythonでのjsonシリアライズが加わるので若干遅くなっていたそうです。
Rustのパフォーマンスに加えて、pythonを経由せずにjsonシリアライズすることになり高速になったそうです。
ベンチマーク
見せてもらおうか、新しいPydanticの性能とやらを!
ということでベンチマークで速くなったのか検証します。
方法
以下の2つのエンドポイントを作成し、負荷テストで検証したいと思います。
- 型ヒントなし
-
response_modelを使う方法 - 戻り値の型ヒント(Return Type)を使う方法
大量のデータを返すシナリオが最も差が出やすいため、1,000個のアイテムを返す設定で比較します。(どんなエンドポイントや)
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
is_active: bool
# ダミーデータ1,000件生成
test_data: list[User] = [
User(
id=i,
name=f"User{i}",
email=f"user{i}@example.com",
is_active=i % 2 == 0
)
for i in range(1000)
]
@app.get("/untyped")
def get_users_old_way():
"""型ヒントなし"""
return test_data
@app.get("/typed")
def get_users_new_way() -> list[User]:
"""戻り値型ヒントを使用"""
return test_data
@app.get("/response-model", response_model=list[User])
def get_users_response_model():
"""response_modelを使用"""
return test_data
@app.get("/health")
def health_check():
"""ヘルスチェック用"""
return {"status": "ok"}
検証
こちらも2種で比較します。
- 計測ツールheyを使って、それぞれに10000回リクエストを送る。
- pythonで計測用スクリプトを書いて、各エンドポイントに対して 100回〜500回 程度のリクエストを送信する。
結果
10000回リクエスト
- 型ヒントなし
$ hey -n 10000 -c 50 http://127.0.0.1:8000/untyped
Summary:
Total: 62.2807 secs
Slowest: 0.5658 secs
Fastest: 0.0204 secs
Average: 0.3101 secs
Requests/sec: 160.5634
Total data: 751710000 bytes
Size/request: 75171 bytes
Response time histogram:
0.020 [1] |
0.075 [10] |
0.130 [24] |
0.184 [135] |■
0.239 [867] |■■■■■■■■
0.293 [2685] |■■■■■■■■■■■■■■■■■■■■■■■■■■
0.348 [4123] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.402 [1379] |■■■■■■■■■■■■■
0.457 [563] |■■■■■
0.511 [187] |■■
0.566 [26] |
Latency distribution:
10%% in 0.2372 secs
25%% in 0.2774 secs
50%% in 0.3056 secs
75%% in 0.3401 secs
90%% in 0.3874 secs
95%% in 0.4230 secs
99%% in 0.4797 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0000 secs, 0.0000 secs, 0.0027 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
req write: 0.0000 secs, 0.0000 secs, 0.0007 secs
resp wait: 0.3100 secs, 0.0176 secs, 0.5658 secs
resp read: 0.0000 secs, 0.0000 secs, 0.0097 secs
Status code distribution:
[200] 10000 responses
平均0.3101秒、Requests/sec: 160.5634でした。
思ったより処理に時間かかっていそうです。
- response_modelを使う方法
$ hey -n 10000 -c 50 http://127.0.0.1:8000/response-model
Summary:
Total: 5.1662 secs
Slowest: 0.0937 secs
Fastest: 0.0057 secs
Average: 0.0258 secs
Requests/sec: 1935.6448
Total data: 751710000 bytes
Size/request: 75171 bytes
Response time histogram:
0.006 [1] |
0.014 [4] |
0.023 [1748] |■■■■■■■■■
0.032 [7825] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.041 [122] |■
0.050 [94] |
0.058 [155] |■
0.067 [1] |
0.076 [19] |
0.085 [20] |
0.094 [11] |
Latency distribution:
10%% in 0.0227 secs
25%% in 0.0237 secs
50%% in 0.0247 secs
75%% in 0.0258 secs
90%% in 0.0275 secs
95%% in 0.0300 secs
99%% in 0.0541 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0000 secs, 0.0000 secs, 0.0022 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
req write: 0.0000 secs, 0.0000 secs, 0.0009 secs
resp wait: 0.0257 secs, 0.0056 secs, 0.0914 secs
resp read: 0.0000 secs, 0.0000 secs, 0.0163 secs
Status code distribution:
[200] 10000 responses
平均レイテンシ0.0258秒、Requests/sec: 1935.6448でした。
型ヒントなしに比べて12倍以上のパフォーマンス差をつけてます。
普通にえぐいな...
- 戻り値の型ヒント(Return Type)を使う方法
% hey -n 10000 -c 50 http://127.0.0.1:8000/typed
Summary:
Total: 5.0625 secs
Slowest: 0.0641 secs
Fastest: 0.0049 secs
Average: 0.0252 secs
Requests/sec: 1975.3149
Total data: 751710000 bytes
Size/request: 75171 bytes
Response time histogram:
0.005 [1] |
0.011 [3] |
0.017 [10] |
0.023 [990] |■■■■■
0.029 [8492] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.034 [237] |■
0.040 [20] |
0.046 [57] |
0.052 [70] |
0.058 [111] |■
0.064 [9] |
Latency distribution:
10%% in 0.0226 secs
25%% in 0.0235 secs
50%% in 0.0245 secs
75%% in 0.0256 secs
90%% in 0.0270 secs
95%% in 0.0286 secs
99%% in 0.0531 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0000 secs, 0.0000 secs, 0.0018 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
req write: 0.0000 secs, 0.0000 secs, 0.0009 secs
resp wait: 0.0252 secs, 0.0048 secs, 0.0640 secs
resp read: 0.0000 secs, 0.0000 secs, 0.0059 secs
Status code distribution:
[200] 10000 responses
平均0.0252秒、1975.31 req/secでした。
こちらも同様に12倍以上の差をつけています。
response_modelと比べて0.0006秒短縮されました。一応パフォーマンス的にはどちらもあまり変わりませんが、体感こちらの方が早いケースが多かったです。
100回〜500回 程度のリクエスト
以下のような計測ツールで検証します。
ベンチマークスクリプト
import asyncio
import time
import statistics
import sys
import httpx
from typing import List
async def benchmark_endpoint(client: httpx.AsyncClient, url: str, requests_count: int) -> dict:
"""エンドポイントのベンチマークを実行"""
response_times = []
print(f"\n🔄 Testing {url} with {requests_count} requests...")
# ウォームアップ(最初の数回は除外)
for _ in range(5):
await client.get(url)
start_time = time.perf_counter()
for i in range(requests_count):
request_start = time.perf_counter()
response = await client.get(url)
request_end = time.perf_counter()
if response.status_code != 200:
print(f"❌ Error: Status code {response.status_code}")
continue
response_times.append((request_end - request_start) * 1000) # ms
if (i + 1) % 100 == 0:
print(f" Progress: {i + 1}/{requests_count} requests completed")
total_time = time.perf_counter() - start_time
return {
"url": url,
"total_requests": requests_count,
"total_time": total_time,
"avg_response_time": statistics.mean(response_times),
"median_response_time": statistics.median(response_times),
"min_response_time": min(response_times),
"max_response_time": max(response_times),
"rps": requests_count / total_time,
"stdev": statistics.stdev(response_times) if len(response_times) > 1 else 0
}
async def run_benchmarks(base_url: str, requests_count: int):
"""すべてのベンチマークを実行"""
async with httpx.AsyncClient(base_url=base_url, timeout=30.0) as client:
# ヘルスチェック
print("🏥 Health check...")
health_response = await client.get("/health")
if health_response.status_code != 200:
print("❌ Server is not healthy!")
sys.exit(1)
print("✅ Server is healthy\n")
# ベンチマーク実行
results = []
# Untyped (型指定なし)
untyped_result = await benchmark_endpoint(client, "/untyped", requests_count)
results.append(("Untyped (no annotation)", untyped_result))
# 少し待機
await asyncio.sleep(1)
# Typed (戻り値型ヒント)
typed_result = await benchmark_endpoint(client, "/typed", requests_count)
results.append(("Typed (return annotation)", typed_result))
# 少し待機
await asyncio.sleep(1)
# Response model
response_model_result = await benchmark_endpoint(client, "/response-model", requests_count)
results.append(("Response Model", response_model_result))
return results
def print_report(results: List[tuple], python_version: str, fastapi_version: str, pydantic_version: str):
"""結果レポートを出力"""
print("\n" + "="*80)
print("📊 BENCHMARK REPORT")
print("="*80)
print("\n【実行環境】")
print(f" Python: {python_version}")
print(f" FastAPI: {fastapi_version}")
print(f" Pydantic: {pydantic_version}")
print("\n【比較表】")
print("-"*80)
print(f"{'Method':<25} {'Avg (ms)':<12} {'Median (ms)':<12} {'RPS':<12} {'Stdev (ms)':<12}")
print("-"*80)
for name, result in results:
print(f"{name:<25} {result['avg_response_time']:>10.2f} "
f"{result['median_response_time']:>10.2f} "
f"{result['rps']:>10.2f} "
f"{result['stdev']:>10.2f}")
print("-"*80)
# 比較分析
untyped = results[0][1]
typed = results[1][1]
response_model = results[2][1]
print("\n【考察】")
print("-"*80)
# 最速を特定
fastest_idx = min(range(len(results)), key=lambda i: results[i][1]['avg_response_time'])
fastest_name = results[fastest_idx][0]
print(f"🏆 最速: {fastest_name}\n")
# Untyped vs Typed
untyped_vs_typed = ((untyped['avg_response_time'] - typed['avg_response_time'])
/ untyped['avg_response_time'] * 100)
print(f"📊 Untyped vs Typed (return annotation):")
if untyped_vs_typed > 0:
print(f" ✅ Typed の方が {untyped_vs_typed:.2f}% 高速")
else:
print(f" ⚠️ Untyped の方が {abs(untyped_vs_typed):.2f}% 高速")
print(f" {untyped['avg_response_time']:.2f}ms → {typed['avg_response_time']:.2f}ms")
print(f" RPS: {untyped['rps']:.2f} → {typed['rps']:.2f} ({((typed['rps'] - untyped['rps']) / untyped['rps'] * 100):+.2f}%)")
# Untyped vs Response Model
untyped_vs_rm = ((untyped['avg_response_time'] - response_model['avg_response_time'])
/ untyped['avg_response_time'] * 100)
print(f"\n📊 Untyped vs Response Model:")
if untyped_vs_rm > 0:
print(f" ✅ Response Model の方が {untyped_vs_rm:.2f}% 高速")
else:
print(f" ⚠️ Untyped の方が {abs(untyped_vs_rm):.2f}% 高速")
print(f" {untyped['avg_response_time']:.2f}ms → {response_model['avg_response_time']:.2f}ms")
print(f" RPS: {untyped['rps']:.2f} → {response_model['rps']:.2f} ({((response_model['rps'] - untyped['rps']) / untyped['rps'] * 100):+.2f}%)")
# Typed vs Response Model
typed_vs_rm = ((typed['avg_response_time'] - response_model['avg_response_time'])
/ typed['avg_response_time'] * 100)
print(f"\n📊 Typed vs Response Model:")
if typed_vs_rm > 0:
print(f" ✅ Response Model の方が {typed_vs_rm:.2f}% 高速")
else:
print(f" ⚠️ Typed の方が {abs(typed_vs_rm):.2f}% 高速")
print(f" {typed['avg_response_time']:.2f}ms → {response_model['avg_response_time']:.2f}ms")
print(f" RPS: {typed['rps']:.2f} → {response_model['rps']:.2f} ({((response_model['rps'] - typed['rps']) / typed['rps'] * 100):+.2f}%)")
print("\n💡 結論:")
if abs(typed_vs_rm) < 5:
print(" Typed (戻り値型ヒント) と Response Model のパフォーマンス差は微小(5%未満)。")
print(" 型ヒントの方がPythonらしい記述ができるため、可読性の観点から推奨されます。")
elif typed_vs_rm < -5:
print(" Typed (戻り値型ヒント) が Response Model より明確に高速です。")
else:
print(" Response Model が Typed より明確に高速です。")
if abs(untyped_vs_typed) > 5 or abs(untyped_vs_rm) > 5:
print(" 型指定(Typed or Response Model)を行うことでパフォーマンスへの影響があります。")
print("="*80)
print()
async def main():
"""メイン処理"""
import fastapi
import pydantic
# バージョン情報取得
python_version = sys.version.split()[0]
fastapi_version = fastapi.__version__
pydantic_version = pydantic.__version__
print("="*80)
print("🚀 FastAPI Performance Benchmark")
print(" Untyped vs Typed (return annotation) vs Response Model")
print("="*80)
base_url = "http://127.0.0.1:8000"
requests_count = 500 # リクエスト数
print(f"\n⚙️ Configuration:")
print(f" Server: {base_url}")
print(f" Requests per endpoint: {requests_count}")
# ベンチマーク実行
results = await run_benchmarks(base_url, requests_count)
# レポート出力
print_report(results, python_version, fastapi_version, pydantic_version)
if __name__ == "__main__":
asyncio.run(main())
1回目
$ python benchmark.py
================================================================================
🚀 FastAPI Performance Benchmark
Untyped vs Typed (return annotation) vs Response Model
================================================================================
⚙️ Configuration:
Server: http://127.0.0.1:8000
Requests per endpoint: 500
🏥 Health check...
✅ Server is healthy
🔄 Testing /untyped with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
🔄 Testing /typed with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
🔄 Testing /response-model with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
================================================================================
📊 BENCHMARK REPORT
================================================================================
【実行環境】
Python: 3.13.0
FastAPI: 0.131.0
Pydantic: 2.12.5
【比較表】
--------------------------------------------------------------------------------
Method Avg (ms) Median (ms) RPS Stdev (ms)
--------------------------------------------------------------------------------
Untyped (no annotation) 7.14 7.04 140.06 0.57
Typed (return annotation) 1.35 1.33 740.09 0.18
Response Model 1.34 1.32 745.92 0.14
--------------------------------------------------------------------------------
【考察】
--------------------------------------------------------------------------------
🏆 最速: Response Model
📊 Untyped vs Typed (return annotation):
✅ Typed の方が 81.08% 高速
7.14ms → 1.35ms
RPS: 140.06 → 740.09 (+428.39%)
📊 Untyped vs Response Model:
✅ Response Model の方が 81.23% 高速
7.14ms → 1.34ms
RPS: 140.06 → 745.92 (+432.56%)
📊 Typed vs Response Model:
✅ Response Model の方が 0.78% 高速
1.35ms → 1.34ms
RPS: 740.09 → 745.92 (+0.79%)
💡 結論:
Typed (戻り値型ヒント) と Response Model のパフォーマンス差は微小(5%未満)。
型ヒントの方がPythonらしい記述ができるため、可読性の観点から推奨されます。
型指定(Typed or Response Model)を行うことでパフォーマンスへの影響があります。
================================================================================
2回目
$ python benchmark.py
================================================================================
🚀 FastAPI Performance Benchmark
Untyped vs Typed (return annotation) vs Response Model
================================================================================
⚙️ Configuration:
Server: http://127.0.0.1:8000
Requests per endpoint: 500
🏥 Health check...
✅ Server is healthy
🔄 Testing /untyped with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
🔄 Testing /typed with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
🔄 Testing /response-model with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
================================================================================
📊 BENCHMARK REPORT
================================================================================
【実行環境】
Python: 3.13.0
FastAPI: 0.131.0
Pydantic: 2.12.5
【比較表】
--------------------------------------------------------------------------------
Method Avg (ms) Median (ms) RPS Stdev (ms)
--------------------------------------------------------------------------------
Untyped (no annotation) 7.25 6.99 137.96 2.03
Typed (return annotation) 1.29 1.27 775.41 0.11
Response Model 1.54 1.29 648.94 2.27
--------------------------------------------------------------------------------
【考察】
--------------------------------------------------------------------------------
🏆 最速: Typed (return annotation)
📊 Untyped vs Typed (return annotation):
✅ Typed の方が 82.21% 高速
7.25ms → 1.29ms
RPS: 137.96 → 775.41 (+462.06%)
📊 Untyped vs Response Model:
✅ Response Model の方が 78.75% 高速
7.25ms → 1.54ms
RPS: 137.96 → 648.94 (+370.39%)
📊 Typed vs Response Model:
⚠️ Typed の方が 19.49% 高速
1.29ms → 1.54ms
RPS: 775.41 → 648.94 (-16.31%)
💡 結論:
Typed (戻り値型ヒント) が Response Model より明確に高速です。
型指定(Typed or Response Model)を行うことでパフォーマンスへの影響があります。
================================================================================
3回目
$ python benchmark.py
================================================================================
🚀 FastAPI Performance Benchmark
Untyped vs Typed (return annotation) vs Response Model
================================================================================
⚙️ Configuration:
Server: http://127.0.0.1:8000
Requests per endpoint: 500
🏥 Health check...
✅ Server is healthy
🔄 Testing /untyped with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
🔄 Testing /typed with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
🔄 Testing /response-model with 500 requests...
Progress: 100/500 requests completed
Progress: 200/500 requests completed
Progress: 300/500 requests completed
Progress: 400/500 requests completed
Progress: 500/500 requests completed
================================================================================
📊 BENCHMARK REPORT
================================================================================
【実行環境】
Python: 3.13.0
FastAPI: 0.131.0
Pydantic: 2.12.5
【比較表】
--------------------------------------------------------------------------------
Method Avg (ms) Median (ms) RPS Stdev (ms)
--------------------------------------------------------------------------------
Untyped (no annotation) 7.72 7.25 129.47 3.48
Typed (return annotation) 1.29 1.26 773.63 0.13
Response Model 1.29 1.25 772.33 0.18
--------------------------------------------------------------------------------
【考察】
--------------------------------------------------------------------------------
🏆 最速: Typed (return annotation)
📊 Untyped vs Typed (return annotation):
✅ Typed の方が 83.27% 高速
7.72ms → 1.29ms
RPS: 129.47 → 773.63 (+497.54%)
📊 Untyped vs Response Model:
✅ Response Model の方が 83.24% 高速
7.72ms → 1.29ms
RPS: 129.47 → 772.33 (+496.53%)
📊 Typed vs Response Model:
⚠️ Typed の方が 0.17% 高速
1.29ms → 1.29ms
RPS: 773.63 → 772.33 (-0.17%)
💡 結論:
Typed (戻り値型ヒント) と Response Model のパフォーマンス差は微小(5%未満)。
型ヒントの方がPythonらしい記述ができるため、可読性の観点から推奨されます。
型指定(Typed or Response Model)を行うことでパフォーマンスへの影響があります。
================================================================================
倍どころじゃないやん...
何度か試してみましたが4倍以上のパフォーマンスを出すケースが多かったです。
総評
2倍どころじゃないやん...
型ヒントがない場合に比べて明らかに速くなっています。
特に大量のリクエストを捌くときに真価を発揮してます。
終わりに
Pythonの型ヒントは保守性の面で付けられているた、性能自体が向上するわけではありません。2,3年ぐらい前Pythonサーバーサイドで厳密に型を定義して開発してたのですが、パフォーマンスに寄与しないと聞いてガッカリしたのを思い出しました。
しかしFastAPIではパフォーマンスにも影響を与えるようになりました。
そう言った事例が生まれる事は嬉しいし、どんどん広まって欲しいなと思いました。
ちなみに今FastAPIを使っている人は
- 0.131.0にアップデート。
- 戻り値に型ヒントを追加。
するだけで今回のようなパフォーマンスの恩恵を得られます。
ぜひアップデートしてみましょう!
Discussion
PRにreturn type or response modelとあるのでresponse modelでの指定も高速化の対象ではないでしょうか?
高速化の対象にならないのはresponse class指定の場合に見えます。
PR中で高速化されたと言及されているベンチマークでもresponse modelのみの指定でした
コメントありがとうございます!
すいませんそうっぽいですね...手元で検証したところものすごい差が出たので修正しますね...
ご指摘ありがとうございます🙇♂️
だった今修正しました!
改めてご指摘ありがとうございます!