Comment ajouter un cache à une application Flask

 

Dans ce tutoriel, nous allons ajouter un cache à une application Flask avec l’extension Flask-Caching, pour répondre plus vite et soulager la base de données ou les API appelées derrière. Nous commencerons par un cache en mémoire pour comprendre le fonctionnement, puis nous passerons à Redis pour la production.

Dans cet article :

  • Installation de Flask-Caching
  • Un premier cache en mémoire avec SimpleCache
  • Une clé de cache par utilisateur
  • Invalider le cache
  • Passer en production avec Redis
  • Lire et écrire directement dans le cache
  • Exemple complet avec Redis

Pré-requis : être à l’aise avec Flask. Si vous débutez, lisez d’abord Python : Comment faire une api web avec Flask.

Installation de Flask-Caching

pip install Flask-Caching

Les exemples de cet article ont été testés avec Flask 3.1.3 et Flask-Caching 2.5.1.

Un premier cache en mémoire avec SimpleCache

Le backend SimpleCache garde les données en mémoire, dans le processus Python. Il ne demande aucune installation, ce qui en fait un bon point de départ pour voir comment fonctionne l’extension.

Attention, avec Gunicorn et plusieurs workers, chaque worker est un processus séparé qui a son propre cache : une même page est calculée une fois par worker, et une invalidation faite par l’un d’eux ne touche pas les autres. Ce n’est pas gênant en développement, mais en production on passe plutôt à Redis.

Initialisation

from flask import Flask
from flask_caching import Cache

app = Flask(__name__)
# Type "SimpleCache": en mémoire, non partagé entre processus
app.config["CACHE_TYPE"] = "SimpleCache"
app.config["CACHE_DEFAULT_TIMEOUT"] = 300  # 5 minutes
cache = Cache(app)

CACHE_DEFAULT_TIMEOUT est la durée de vie par défaut d’une entrée (son TTL), en secondes. Chaque décorateur peut la changer avec son paramètre timeout.

Mettre en cache une route

# Met en cache toute la vue pendant 60 secondes
@app.route("/time")
@cache.cached(timeout=60)
def server_time():
    import time
    return {"server_time": time.time()}

Pendant 60 secondes, tous les appels à /time renvoient la même valeur, sans exécuter la fonction. Notez l’ordre des décorateurs : @app.route en premier, puis @cache.cached.

Tenir compte de la query string

Par défaut, la clé de cache est construite à partir du chemin de la requête : /search?q=flask et /search?q=django partageraient la même entrée. Avec query_string=True, les paramètres de l’URL font partie de la clé :

# Met en cache en fonction de la query string (?page=, ?q=, ...)
from flask import request
from time import sleep

@app.route("/search")
@cache.cached(timeout=120, query_string=True)
def search():
    # Simule une opération coûteuse
    sleep(1)
    # On renvoie simplement les paramètres pour l'exemple
    return {"q": request.args.get("q"), "page": request.args.get("page", 1)}

Le premier appel à /search?q=flask&page=2 prend une seconde, les suivants sont immédiats. Flask-Caching trie les paramètres avant de calculer la clé : /search?page=2&q=flask tombe sur la même entrée.

Mémoriser une fonction coûteuse avec memoize

@cache.cached met en cache la réponse d’une vue. Pour mettre en cache le résultat d’une fonction Python selon ses arguments, on utilise @cache.memoize :

@cache.memoize(timeout=300)
def compute_stats(user_id: int):
    # Ici, faites des requêtes SQL lourdes, appels API, etc.
    import time
    time.sleep(2)
    return {"user_id": user_id, "score": 42}

On l’appelle ensuite normalement, par exemple depuis une route :

@app.route("/users/<int:user_id>/stats")
def user_stats(user_id: int):
    return compute_stats(user_id)

Le premier appel à /users/123/stats prend deux secondes, les suivants sont immédiats. compute_stats(456) a sa propre entrée dans le cache.

Une clé de cache par utilisateur

Une page personnalisée ne doit pas être servie à tout le monde : si la clé de cache ne contient pas l’identité de l’utilisateur, le premier visiteur voit sa page, et tous les suivants la voient aussi. On peut passer à key_prefix une fonction qui construit la clé, ici avec l’identifiant de l’utilisateur connecté.

L’exemple s’appuie sur Flask-Login pour accéder à current_user. Il faut donc l’avoir installé (pip install flask-login) et configuré dans votre application.

from flask_login import current_user

@app.route("/dashboard")
@cache.cached(timeout=120, key_prefix=lambda: f"dashboard:{getattr(current_user, 'id', 'anon')}")
def dashboard():
    # Rendu personnalisé
    return {"hello": getattr(current_user, 'id', 'anon')}

L’utilisateur 123 obtient la clé dashboard:123, et les visiteurs non connectés partagent la clé dashboard:anon.

Le même problème se pose quand la réponse dépend d’un en-tête, la langue par exemple. query_string=True ne regarde que l’URL, il faut donc ajouter la langue à la clé soi-même :

from flask import request
from urllib.parse import urlencode
import hashlib

def qs_key(prefix: str = "view"):
    # Clé stable : langue + paramètres triés (ex: items:fr:3f2a...)
    lang = request.accept_languages.best_match(["fr", "en"]) or "fr"
    args = urlencode(sorted(request.args.items(multi=True)))
    h = hashlib.sha1(args.encode()).hexdigest()
    return f"{prefix}:{lang}:{h}"

@app.route("/items")
@cache.cached(timeout=120, key_prefix=lambda: qs_key("items"))
def list_items():
    # ... charge la liste filtrée/paginée ...
    return {"items": [1, 2, 3]}

Comme avec query_string=True, les paramètres sont triés avant d’être hachés, pour que ?q=flask&page=2 et ?page=2&q=flask donnent la même clé.

Invalider le cache

Quand une donnée change, il faut supprimer l’entrée correspondante pour ne pas servir une version périmée jusqu’à la fin du TTL. Flask-Caching propose trois niveaux.

Supprimer une clé précise :

cache.delete("dashboard:123")

Invalider une fonction mémorisée (memoize) :

# Supprime tous les caches de compute_stats, tous arguments confondus
cache.delete_memoized(compute_stats)

# Ou seulement pour un argument précis
cache.delete_memoized(compute_stats, 123)

Tout vider :

cache.clear()

Le bon moment pour invalider est juste après l’écriture en base qui modifie la donnée : après un POST /users/123, on appelle cache.delete_memoized(compute_stats, 123). Évitez de vider tout le cache à chaque modification : une invalidation ciblée suffit, et on n’a pas à tout recalculer.

Passer en production avec Redis

En production, on utilise un backend partagé comme Redis : tous les workers, et toutes les machines, lisent et écrivent dans le même cache.

Installation :

# Redis côté serveur (exemple Ubuntu/Debian)
sudo apt-get install redis-server
# Client Python
pip install redis

Configuration de Flask-Caching pour Redis :

from flask import Flask
from flask_caching import Cache

app = Flask(__name__)
app.config.update(
    CACHE_TYPE="RedisCache",
    CACHE_REDIS_HOST="localhost",  # ou le hostname du service Redis
    CACHE_REDIS_PORT=6379,
    CACHE_REDIS_DB=0,
    CACHE_REDIS_PASSWORD=None,      # définissez un mot de passe si nécessaire
    CACHE_DEFAULT_TIMEOUT=300,
)
cache = Cache(app)

Le reste du code (décorateurs, invalidation) ne change pas. Avec docker compose, CACHE_REDIS_HOST prend le nom du service Redis (redis par exemple).

Lire et écrire directement dans le cache

En dehors des décorateurs, l’objet cache s’utilise comme un dictionnaire avec une durée de vie :

import json
cache.set("key", json.dumps({"x": 1}), timeout=60)
value = json.loads(cache.get("key") or "null")

cache.get() renvoie None si la clé n’existe pas ou a expiré. Passer par JSON n’est pas obligatoire : le backend Redis sérialise les valeurs avec pickle, on peut donc y ranger directement un dictionnaire ou tout objet Python sérialisable. Le JSON a surtout un intérêt si un programme écrit dans un autre langage doit lire le même cache.

Pour le TTL, tout dépend de la fraîcheur attendue : quelques dizaines de secondes pour une liste paginée qui bouge souvent, plusieurs minutes, voire une heure, pour des données qui changent rarement.

Exemple complet avec Redis

Pour finir, une petite API qui met en cache la lecture d’un produit et invalide l’entrée quand le produit est modifié :

from flask import Flask
from flask_caching import Cache

app = Flask(__name__)
app.config.update(
    CACHE_TYPE="RedisCache",
    CACHE_REDIS_HOST="redis",  # docker compose service name
    CACHE_REDIS_PORT=6379,
    CACHE_DEFAULT_TIMEOUT=300,
)
cache = Cache(app)

@cache.memoize(timeout=120)
def get_product(pid: int):
    # Simule un accès BDD lourd
    import time
    time.sleep(1)
    return {"id": pid, "name": f"Product {pid}"}

@app.route("/products/<int:pid>")
def product(pid: int):
    data = get_product(pid)
    return data

@app.route("/products/<int:pid>", methods=["PUT"])
def update_product(pid: int):
    # ... update BDD ...
    cache.delete_memoized(get_product, pid)  # invalider le cache du produit
    return {"status": "updated", "id": pid}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Le premier GET /products/7 prend une seconde, les suivants sont immédiats. Après un PUT /products/7, le GET suivant recharge le produit.

Voir aussi