Optimiser les performances d'une API FastAPI : Guide des meilleures pratiques
La plupart des APIs FastAPI lentes ne le sont pas à cause du framework : elles mélangent du code synchrone dans des fonctions async, empilent les requêtes N+1 et cachent au hasard. Voici les cinq chantiers qui donnent le plus de résultats, avec le code et les mesures qui vont avec.
Je me souviens encore de cette nuit blanche de février 2023. Mon API FastAPI venait de planter en production pour la troisième fois en une semaine. 50 000 utilisateurs connectés, des timeouts partout, et moi qui fixais mes logs avec l'amère réalisation que ma maîtrise du développement backend Python (Django & FastAPI) n'était peut-être pas aussi solide que je le pensais. Cette expérience douloureuse m'a forcé à repenser complètement mon approche des performances FastAPI.
Aujourd'hui, après avoir optimisé plus d'une quinzaine d'APIs en production chez TokamDarius, je peux affirmer sans détour : 90% des problèmes de performance FastAPI viennent de mauvaises pratiques de base, pas de limitations techniques du framework. Et je vais vous expliquer exactement comment les éviter.
Cache stratégique
Le caching, c'est comme les fondations d'une maison : si tu le rates au début, tout s'effondre plus tard. J'ai longtemps cru que FastAPI était naturellement rapide et que le cache était optionnel. Erreur monumentale.
Mon premier réflexe était d'ajouter du cache partout, comme un pansement sur une jambe de bois. Redis sur toutes les routes, cache en mémoire sur les fonctions, TTL à 300 secondes par défaut. Résultat ? Des données incohérentes et des bugs impossibles à déboguer.
La vraie stratégie, c'est le cache à plusieurs niveaux :
- Cache applicatif avec
@lru_cachepour les fonctions pures - Cache Redis pour les données partagées entre instances
- Cache HTTP avec les headers appropriés pour les clients
from functools import lru_cache
import redis
from fastapi import FastAPI, Header
@lru_cache(maxsize=1000)
def get_expensive_calculation(param: str):
# Calcul coûteux en CPU
return complex_algorithm(param)
Ce qui m'a sauvé, c'est de mesurer avant d'optimiser. Utilise fastapi-cache pour débuter, mais implemente ton propre système dès que tu dépasses 1000 requêtes par seconde. D'ailleurs, j'ai écrit un article complet sur Django vs FastAPI : mon retour d'expérience sur 15 APIs où je détaille les différences de performance entre les deux.
Gestion asynchrone
Ici, on touche au cœur du problème. FastAPI est asynchrone par design, mais 99% des développeurs l'utilisent de manière synchrone sans s'en rendre compte. Moi le premier.
Mon erreur classique était de mixer du code sync dans des fonctions async :
# MAUVAIS - bloque l'event loop
async def get_user_data(user_id: int):
user = db.query(User).filter(User.id == user_id).first() # Synchrone !
return user
# BON - vraiment asynchrone
async def get_user_data(user_id: int):
async with database.transaction():
user = await database.fetch_one(
"SELECT * FROM users WHERE id = :id", {"id": user_id}
)
return user
La maîtrise du développement backend Python (Django & FastAPI) passe obligatoirement par la compréhension de l'asynchrone. Tu ne peux pas faire semblant.
Mes règles non-négociables :
- SQLAlchemy async avec
asyncpgpour PostgreSQL - httpx au lieu de requests pour les appels HTTP externes
- Dependency injection asynchrone pour les connexions DB
Le piège, c'est que ton code peut fonctionner en développement avec 10 utilisateurs et planter en production avec 1000 connexions simultanées. J'ai appris ça à mes dépens.
Configuration middleware
Les middlewares, c'est le système nerveux de ton API. Mal configurés, ils peuvent transformer ta Ferrari en vélo rouillé. J'ai vu des APIs perdre 70% de leurs performances à cause d'un middleware de logging mal écrit.
Mon middleware de profiling personnel ressemble à ça :
import time
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class PerformanceMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
# Log seulement si > 1 seconde
if process_time > 1.0:
logger.warning(f"Slow request: {request.url} took {process_time:.2f}s")
response.headers["X-Process-Time"] = str(process_time)
return response
L'ordre des middlewares est critique. Mets le profiling en premier, la compression en dernier. CORS avant l'authentification. J'ai passé 6 heures à déboguer un problème d'auth qui venait juste d'un mauvais ordre de middlewares.
Évite les middlewares tiers non-maintenus. J'utilise uniquement :
CORSMiddlewarede StarletteGZipMiddlewarepour la compression- Mon middleware custom de monitoring
Optimisation base de données
C'est là que ça devient sérieux. 95% des problèmes de performance viennent de la base de données, pas de Python. Et FastAPI ne peut pas compenser des requêtes SQL pourries.
Ma règle d'or : une route = une requête SQL maximum. Si tu fais plus, tu es dans le N+1 problem et tu vas souffrir.
# MAUVAIS - N+1 queries
async def get_users_with_posts():
users = await database.fetch_all("SELECT * FROM users")
for user in users:
user["posts"] = await database.fetch_all(
"SELECT * FROM posts WHERE user_id = :id", {"id": user["id"]}
)
return users
# BON - Une seule requête avec JOIN
async def get_users_with_posts():
return await database.fetch_all("""
SELECT u.id, u.name, p.title, p.content
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
""")
J'utilise SQLAlchemy Core avec FastAPI, pas l'ORM. Plus verbeux, mais les performances sont incomparables. Pour des APIs haute performance, l'ORM est un luxe qu'on ne peut pas se permettre.
Connection pooling obligatoire :
from databases import Database
database = Database(
"postgresql://user:pass@host/db",
min_size=5,
max_size=20
)
Monitoring et profiling
Impossible d'optimiser ce qu'on ne mesure pas. J'ai longtemps volé à l'aveuglette, en me fiant à mon "feeling" de développeur. Catastrophique.
Mon stack de monitoring actuel chez TokamDarius :
- Prometheus + Grafana pour les métriques système
- py-spy pour le profiling Python en production
- PostgreSQL logs analysés avec pgBadger
- Custom metrics dans FastAPI avec
prometheus-client
from prometheus_client import Counter, Histogram
import time
REQUEST_COUNT = Counter('requests_total', 'Total requests', ['method', 'endpoint'])
REQUEST_LATENCY = Histogram('request_duration_seconds', 'Request latency')
@app.middleware("http")
async def monitor_requests(request: Request, call_next):
start = time.time()
response = await call_next(request)
REQUEST_COUNT.labels(
method=request.method,
endpoint=request.url.path
).inc()
REQUEST_LATENCY.observe(time.time() - start)
return response
Ce qui m'a ouvert les yeux, c'est de profiler en production avec des vrais utilisateurs. Mes tests de charge en local ne reflétaient pas la réalité. Les patterns d'usage réels révèlent des goulots d'étranglement invisibles en développement.
Utilise py-spy sans modération :
py-spy record -o profile.svg -d 60 -p $(pgrep -f "uvicorn")
Tu découvriras que 80% de ton temps CPU se passe dans 3 fonctions. Optimise ces 3 fonctions, ignore le reste.
Déploiement production
Le déploiement, c'est là où la théorie rencontre la réalité brutale. J'ai eu ma dose de réveils à 3h du matin à cause de configurations foireuses.
Uvicorn seul en production = suicide professionnel. Utilise Gunicorn avec des workers Uvicorn :
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--access-logfile - \
--error-logfile - \
--log-level info
Calcul du nombre de workers : (2 * CPU cores) + 1. Mais teste en conditions réelles, la théorie ne suffit pas.
Ma configuration Nginx pour FastAPI :
upstream fastapi {
server 127.0.0.1:8000;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
}
server {
location / {
proxy_pass http://fastapi;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
}
}
Health checks obligatoires :
@app.get("/health")
async def health():
try:
await database.fetch_one("SELECT 1")
return {"status": "healthy"}
except:
return {"status": "unhealthy"}
Je ne compte plus les fois où j'ai découvert des problèmes de performance uniquement en production. Load balancer mal configuré, connexions DB qui traînent, memory leaks invisibles en développement... La liste est longue.
D'ailleurs, si tu veux éviter les mêmes erreurs que moi avec Django en production, j'ai documenté mes erreurs les plus coûteuses dans Django en prod : 7 erreurs qui m'ont coûté des nuits.
L'optimisation FastAPI, c'est 20% de technique et 80% de mesure. Tu peux appliquer toutes les bonnes pratiques du monde, si tu ne mesures pas l'impact réel, tu optimises peut-être dans le vide.
Ma recommandation : commence par monitorer ton API actuelle pendant une semaine. Identifie tes 3 endpoints les plus lents. Optimise-les un par un avec les techniques que j'ai partagées. Mesure l'impact. Répète.
La maîtrise du développement backend Python (Django & FastAPI) ne vient pas des tutoriels YouTube ou des cours en ligne. Elle vient des nuits blanches, des bugs en production, et de la volonté d'apprendre de ses erreurs.
Alors arrête de procrastiner et va mesurer les performances de ton API dès maintenant. Tu me remercieras dans 6 mois.
Tags
A lire aussi
Jongler avec 6 clients à la fois sans rien laisser tomber
Gérer six projets clients en solo sans échapper une démo ni brûler ses weekends tient à quatre habitudes simples : une revue du lundi, un canal unique par client, trois signaux d'alarme et un tableau de capacité honnête. Voici le système, et pourquoi il tue l'anxiété du dimanche soir.
React + TypeScript : mon stack par défaut, sans exception
Un écran blanc en prod à cause d'une réponse API qui a changé de forme : le genre de bug que TypeScript rend impossible à écrire. Pourquoi React et TypeScript sont devenus mon point de départ non négociable, avec la config exacte que je réutilise sur chaque projet.
Comment j'automatise mon workflow de développement web (et ce que je garde manuel)
Après deux ans en solo, j'ai appris que l'automatisation ne sauve personne du burnout si elle attaque les mauvaises tâches. Voici ce que j'automatise sans hésiter (tests, CI/CD, formatage), ce que je refuse de déléguer, et la grille de décision que j'applique à chaque tâche répétitive.

