Mempercepat dan melindungi aplikasi Flask: caching response memakai Flask-Caching dengan backend Redis atau memori, caching per-view dengan decorator, serta rate limiting dengan Flask-Limiter untuk membatasi request per-user dan per-IP.

Dua masalah datang seiring aplikasi ramai: kinerja (setiap request memproses ulang data yang sama) dan penyalahgunaan (beberapa client membanjiri server). Episode 15 menangani keduanya dengan dua library resmi ekosistem Flask: Flask-Caching untuk menyimpan hasil perhitungan yang mahal, dan Flask-Limiter untuk membatasi jumlah request. Pola ini — cache untuk data yang jarang berubah, rate limit untuk API publik — adalah fondasi aplikasi yang bisa bertahan di bawah beban.
Caching menyimpan hasil komputasi agar request berikutnya tidak mengulanginya — analogi dunia nyata: kantin yang menyiapkan menu andalan lebih awal, pelanggan tidak perlu menunggu dimasak dari nol setiap kali. Alur singkatnya: request masuk → dicek apakah cache-nya ada (cache hit) → jika ya, langsung balas dari cache; jika tidak, proses view + query DB lalu hasilnya disimpan untuk request berikutnya.
Kasus yang paling diuntungkan: list data yang jarang berubah (kategori, konfigurasi), hasil query berat (agregasi), dan template yang mahal dirender.
Install dan daftarkan seperti extension lain:
pip install flask-cachingfrom flask_caching import Cache
cache = Cache()class BaseConfig:
CACHE_TYPE = "RedisCache" # backend Redis
CACHE_REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
CACHE_DEFAULT_TIMEOUT = 300 # 5 menit defaultCACHE_TYPE menentukan backend — untuk development tanpa Redis, pakai "SimpleCache" (memori dalam proses). Redis adalah pilihan produksi: shared antar worker dan tahan restart (kita pakai Redis lagi di episode 16).
Decorator @cache.cached() menyimpan response lengkap sebuah view:
@api_bp.get("/stats")
@cache.cached(timeout=600) # 10 menit
def get_stats() -> dict:
total = db.session.execute(db.select(func.count(User.id))).scalar()
return api_ok({"total_users": total})timeout=600 membuat response valid 10 menit. Request berikutnya dalam rentang itu langsung dilayani dari cache — query database tidak berjalan sama sekali.
Peringatan penting: @cache.cached default membuat cache tidak peduli parameter request — untuk endpoint berparameter, harus membuat key sendiri:
@api_bp.get("/search")
@cache.cached(timeout=300, query_string=True)
def search() -> dict:
q = request.args.get("q", "")
results = run_expensive_search(q)
return api_ok(results)query_string=True memasukkan query string ke dalam kunci cache — sehingga ?q=flask dan ?q=django di-cache terpisah. Jangan pernah memakai @cache.cached di view yang datanya spesifik per-user (misal dashboard) tanpa memecah key per user — atau data pengguna bisa bocor ke pengguna lain.
Warning
Jangan memakai @cache.cached untuk halaman yang menampilkan data pengguna berbeda (dashboard, profil) — response tersimpan satu salinan dan bisa diberikan ke pengguna yang salah. Untuk konten personal, cache potongan data (misal hasil query) bukan response halaman, atau sertakan user_id dalam cache key.
Alternatif yang lebih aman untuk data personal: cache hasil fungsi, bukan response:
@cache.memoize(timeout=600)
def get_user_stats(user_id: int) -> dict:
# query berat - hasilnya sama untuk user_id yang sama
total_posts = db.session.scalar(
db.select(func.count(Post.id)).where(Post.user_id == user_id)
)
return {"user_id": user_id, "total_posts": total_posts}@cache.memoize menyimpan hasil berdasarkan argumen fungsi. Untuk invalidasi manual — saat data berubah — panggil cache.delete_memoized(get_user_stats, user_id) di titik mutasi (misal setelah post dibuat).
Sekarang sisi pertahanan: rate limiting.
pip install flask-limiterfrom flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
limiter.default_limits = ["200 per day", "50 per hour"]key_func=get_remote_address membatasi berdasarkan IP client. default_limits berlaku ke semua route — pengecualian ditangani per-route.
Batasi endpoint spesifik dengan decorator:
from app.extensions import limiter
@api_bp.post("/login")
@limiter.limit("10 per minute")
def login() -> tuple[dict, int]:
...
@api_bp.get("/users")
@limiter.limit("60 per minute")
def list_users() -> tuple[dict, int]:
...Untuk membatasi berdasarkan user (bukan IP) — penting saat beberapa user berbagi IP di belakang NAT — buat key func custom:
from flask_login import current_user
def user_or_ip() -> str:
if current_user.is_authenticated:
return f"user:{current_user.id}"
return get_remote_address()
limiter = Limiter(key_func=user_or_ip)Dengan key user:{id}, user yang login punya kuota sendiri; yang belum login dihitung per IP. Rate limit juga bisa dipakai sebagai pertahanan brute force login — kita singgung di episode 13.
Pola akhir yang direkomendasikan untuk API publik:
@api_bp.get("/categories")
@limiter.limit("120 per minute")
@cache.cached(timeout=1800)
def get_categories() -> tuple[dict, int]:
rows = db.session.scalars(db.select(Category).order_by(Category.id)).all()
return api_ok([{"id": c.id, "name": c.name} for c in rows])Order decorator penting: @limiter.limit dieksekusi lebih dulu, jadi request yang melebihi kuota ditolak sebelum menyentuh cache. @cache.cached kemudian melayani data yang sudah jadi tanpa menyentuh database.
@cache.cached tanpa key di view berparameter: semua query string berbagi satu cache — data silang.delete_memoized/delete saat mutasi.10 per minute) di endpoint auth.get_remote_address perlu konfigurasi proxy agar mendeteksi IP asli (episode 22).Pada episode 15 ini, kalian telah mempercepat dan melindungi aplikasi.
Inti yang harus dibawa pulang:
@cache.cached untuk response, @cache.memoize untuk hasil fungsi.RedisCache untuk produksi; SimpleCache untuk development.default_limits global + @limiter.limit per endpoint.user:{id}) untuk limit yang adil di belakang NAT.Di episode 16 selanjutnya, kita menangani pekerjaan yang tidak boleh memblokir request: background tasks — threading, RQ dengan Redis Queue, hingga Celery untuk job kompleks seperti mengirim email dan generate report. Sampai jumpa di episode 16!