Belajar Flask - Caching & Rate Limiting
Episode 15 of 27

Belajar Flask - Caching & Rate Limiting

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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: Menghindari Pekerjaan Ulang

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.

Setup Flask-Caching

Install dan daftarkan seperti extension lain:

Install Flask-Caching
pip install flask-caching
Pythonapp/extensions.py
from flask_caching import Cache
 
cache = Cache()
Pythonapp/__init__.py - konfigurasi dan init
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 default

CACHE_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).

Per-View Caching: @cache.cached

Decorator @cache.cached() menyimpan response lengkap sebuah view:

PythonCache per 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:

PythonCache dengan key per parameter
@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.

Mem-cache Hasil Fungsi, Bukan Response

Alternatif yang lebih aman untuk data personal: cache hasil fungsi, bukan response:

PythonCache hasil fungsi berat
@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).

Setup Flask-Limiter

Sekarang sisi pertahanan: rate limiting.

Install Flask-Limiter
pip install flask-limiter
Pythonapp/extensions.py + __init__.py
from 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.

Limit Per-User dan Per-IP

Batasi endpoint spesifik dengan decorator:

PythonRate limit per endpoint
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:

PythonRate limit berdasarkan user
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.

Kombinasi Cache + Rate Limit untuk API

Pola akhir yang direkomendasikan untuk API publik:

  1. Rate limit di lapisan paling luar: cegah banjir request sebelum menyentuh view.
  2. Cache untuk endpoint read yang jarang berubah: kurangi beban database.
  3. Cache-busting untuk data yang berubah (memoize + invalidate).
PythonAPI dengan rate limit dan cache
@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.

Common Pitfalls Caching & Rate Limit

  • @cache.cached tanpa key di view berparameter: semua query string berbagi satu cache — data silang.
  • Cache response personal: bocor data antar user — gunakan memoize atau key per user.
  • Invalidate lupa: data berubah tapi cache basi — panggil delete_memoized/delete saat mutasi.
  • Limit terlalu longgar di login: brute force password — pakai limit ketat (10 per minute) di endpoint auth.
  • Limiter pakai IP di belakang proxy: get_remote_address perlu konfigurasi proxy agar mendeteksi IP asli (episode 22).

Penutup

Pada episode 15 ini, kalian telah mempercepat dan melindungi aplikasi.

Inti yang harus dibawa pulang:

  • Flask-Caching: @cache.cached untuk response, @cache.memoize untuk hasil fungsi.
  • Backend RedisCache untuk produksi; SimpleCache untuk development.
  • Jangan cache data personal tanpa memecah key per user.
  • Flask-Limiter: default_limits global + @limiter.limit per endpoint.
  • Key func custom (user:{id}) untuk limit yang adil di belakang NAT.
  • Susun rate limit di luar, cache di dalam — request diblokir sebelum menyentuh data.

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!

Belajar Flask - Caching & Rate Limiting | Belajar Flask