Mengukur dan mengoptimalkan performa FastAPI: profiling endpoint dengan cProfile dan middleware timing, tuning connection pooling database, caching dengan Redis, serta load testing dengan k6 untuk menemukan bottleneck.

Setelah di episode 24 aplikasi kalian dideploy ke produksi, pertanyaan berikutnya selalu muncul: seberapa cepat, dan seberapa siap? Episode ini membahas cara mengukur performa dengan benar — bukan menebak — lalu mengoptimalkan hal yang paling sering jadi bottleneck: database, koneksi, dan cache.
Mengapa episode ini penting? "Optimasi" tanpa pengukuran hanyalah intuisi. Episode ini mengajarkan urutan yang benar: ukur → temukan bottleneck → perbaiki → ukur lagi. Dan kita akan membuktikan bahwa perbaikan paling berdampak di FastAPI biasanya bukan di kode Python, melainkan di database dan cache.
Sejak episode 11 kita punya dasar timing middleware. Perluas menjadi logging per endpoint untuk menemukan endpoint yang lambat:
import time
import logging
from fastapi import FastAPI, Request
logger = logging.getLogger("perf")
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration_ms = (time.perf_counter() - start) * 1000
if duration_ms > 500:
logger.warning(
"slow request path=%s method=%s duration_ms=%.1f",
request.url.path, request.method, duration_ms,
)
response.headers["X-Process-Time"] = f"{duration_ms:.2f}"
return responseAmbang batas (500ms) membuat log hanya berisik saat ada masalah — teknik observability dari episode 17 yang dipakai praktis.
Untuk menemukan fungsi mana yang menghabiskan waktu, pakai cProfile — profiler bawaan Python:
python -m cProfile -s cumtime -o profile.out \
-c "from fastapi.testclient import TestClient; from app.main import app; c=TestClient(app); c.get('/items/')"import pstats
p = pstats.Stats("profile.out")
p.sort_stats("cumtime").print_stats(20)Output menampilkan 20 fungsi teratas berdasarkan cumulative time — di sinilah kalian melihat apakah waktu habis di query SQL, serialisasi Pydantic, atau kode bisnis. Jangan mengoptimasi tanpa bukti dari output ini.
Tip
Aturan 90/10 performa FastAPI: 90% masalah ada di database (query N+1, index hilang, pooling kecil) dan I/O blocking — bukan di "Python yang lambat". Mulai profil dari lapisan query, bukan dari micro-optimasi sintaks.
Bottleneck paling umum kedua setelah query lambat: koneksi database. Pooling yang kita konfigurasi di episode 14 punya angka yang bisa disetel:
engine = create_async_engine(
DATABASE_URL,
pool_size=20,
max_overflow=10,
pool_pre_ping=True,
pool_recycle=1800,
)Tanda-tanda pooling bermasalah:
TimeoutError: QueuePool limit of size ... overflow ... reached → pool terlalu kecil untuk concurrency; naikkan pool_size/max_overflow (atau periksa query yang terlalu lama).pool_pre_ping=True menyelamatkan.Important
Jangan menaikkan pool tanpa ukur — koneksi database adalah resource terbatas (PostgreSQL default 100). Pool 100 pada 10 worker = database kebanjiran. Sesuaikan pool dengan batas database dan beban aktual yang diukur lewat benchmark.
Bottleneck ketiga yang paling berdampak besar: query berulang untuk data yang jarang berubah. Redis menjawabnya dengan cache di memory:
pip install redisimport json
import redis.asyncio as aioredis
from fastapi import FastAPI
app = FastAPI()
cache = aioredis.from_url(
"redis://localhost:6379/0", decode_responses=True
)
CACHE_TTL = 60 # detik
@app.get("/items/")
async def list_items() -> dict:
cached = await cache.get("items:list")
if cached is not None:
return json.loads(cached)
items = await fetch_items_from_db() # query mahal
await cache.set("items:list", json.dumps(items), ex=CACHE_TTL)
return {"items": items}Pola cache-aside: cek cache → jika ada, langsung kembalikan; jika tidak, hitung dari database → simpan ke cache → kembalikan. Request berikutnya dalam 60 detik tidak menyentuh database sama sekali.
Warning
Cache menambahkan masalah stale data (data basi). Tetapkan TTL realistis dan invalidate cache saat data diubah (hapus key di endpoint write). Untuk data yang berubah cepat, jangan di-cache — atau terima keterlambatan konsistensi dengan sadar.
Benchmark dimulai dengan k6 — tool load testing yang modern dan bisa dijalankan di CI:
# Linux/macOS (Homebrew)
brew install k6import http from "k6/http";
import { check } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 20 }, // rampa ke 20 VU
{ duration: "1m", target: 50 }, // tahan 50 VU
{ duration: "30s", target: 0 }, // turun
],
thresholds: {
http_req_duration: ["p(95)<500"],
http_req_failed: ["rate<0.01"],
},
};
export default function () {
const res = http.get("http://localhost:8000/items/");
check(res, { "status 200": (r) => r.status === 200 });
}Jalankan:
k6 run script.jsOutput k6 menampilkan metrik kunci: http_req_duration (latensi), http_reqs (throughput), dan http_req_failed (error rate). thresholds mengubah benchmark menjadi gerbang otomatis: jika p(95) > 500ms, test gagal — sempurna untuk CI.
Tip
Aturan pembacaan hasil: perhatikan latensi p95 vs p99 (bukan rata-rata — rata-rata menyembunyikan outlier), dan error rate. Benchmark sebelum vs sesudah perubahan (misal menambah cache Redis) adalah cara membuktikan bahwa optimasi benar-benar bekerja.
Urutan perbaikan yang paling berdampak:
pool_pre_ping.response_model (makin sedikit field, makin cepat).| Pitfall | Solusi |
|---|---|
| Optimasi tanpa pengukuran | Profil dulu dengan cProfile / timing |
| Pool dinaikkan tanpa ukur | Sesuaikan dengan batas DB + benchmark |
| Cache tanpa invalidate | Hapus key saat write; TTL realistis |
| Hanya melihat latensi rata-rata | Lihat p95/p99 dan error rate |
| Query N+1 di ORM | selectinload/joinedload untuk relasi |
Inti yang harus dibawa pulang:
QueuePool penuh, pool_pre_ping, sesuaikan dengan batas DB.stages + thresholds mengubah load test jadi gerbang CI.Di episode 26 selanjutnya kita akan membahas ekosistem & tren modern 2026 — posisi FastAPI 0.141, Pydantic 2.x, Starlette 0.4x, perbandingan matang dengan Flask/Django/Express, dan tren AI backends yang membentuk arah framework ini!