Caching responses and rate limiting logins in Flask with Encore, redis-py, Flask-Caching and Flask-Limiter, with expiries that cannot leak keys and a Redis that runs locally.
Flask apps usually reach for Redis when a page gets slow or a login form gets hammered. Adding it means a Redis server for every developer, a hosted Redis in production, connection settings per environment and an expiry on every key.
This guide adds Redis to a Flask app with Encore, redis-py directly, Flask-Caching and Flask-Limiter. The examples cache a product lookup for 60 seconds and limit login attempts to five per minute.
What you need to decide:
Encore combines Redis databases declared in your Flask code with the servers behind them, starting Redis locally under encore run and provisioning ElastiCache on AWS or Memorystore on GCP in your own account when you deploy. It is an infrastructure SDK added to an existing app, so routes, blueprints and extensions stay as they are.

The project needs encore-dev, flask[async] and Waitress in its dependencies, with Python 3.9 or later. The Redis database is one line at module level, and the product view reads through it with a 60 second expiry:
import json
import time
from encore.storage import redis
from flask import Flask, request
# Encore starts this Redis database under `encore run` and provisions
# ElastiCache or Memorystore for it when the app is deployed.
cache = redis.Database("cache")
app = Flask(__name__)
def load_product(product_id: int) -> dict:
# Stand-in for a slow query or an upstream API call.
time.sleep(0.3)
return {"id": product_id, "name": f"Product {product_id}", "price_cents": 1999}
@app.get("/products/<int:product_id>")
async def get_product(product_id: int):
key = f"product:{product_id}"
cached = await cache.string.get[str](key)
if cached is not None:
return {"source": "cache", "product": json.loads(cached)}
product = load_product(product_id)
await cache.string.set(key, json.dumps(product), ex=60)
return {"source": "origin", "product": product}
The login limit creates the counter with its expiry before incrementing it. If the increment came first, a crash between the two calls would leave a counter with no expiry:
@app.post("/login")
async def login():
email = request.get_json()["email"]
key = f"login-attempts:{email}"
# Create the counter with its expiry first, so it never lives forever.
await cache.string.set(key, 0, ex=60, nx=True)
attempts = await cache.string.incr(key)
if attempts > 5:
return {"error": "too many attempts, try again in a minute"}, 429
return {"ok": True, "attempts": attempts}
The entry point serves Flask with Waitress on the address Encore assigns:
import os
from waitress import serve
if __name__ == "__main__":
host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":")
serve(app, host=host or "0.0.0.0", port=int(port))
Under encore run the first request misses, the second is served from Redis, and the sixth login in a minute gets a 429:
Each request gets a trace that shows the Redis calls next to the handler. On deploy, Encore provisions ElastiCache on AWS or Memorystore on GCP in your own account and passes the connection to the service.
An agent working on the app can add a second cache or counter as another redis.Database and check the hit rate in local traces.
Treat Redis as a cache that can be empty, so views fall back to the source when a key is missing. The INCR command is atomic, which is why the counter is safe under concurrent requests; the Redis docs for INCR describe this rate limiter pattern.
The official client works in any Flask app. Create the synchronous client once and share it across requests:
import json
import os
import redis
from flask import Flask
app = Flask(__name__)
r = redis.Redis.from_url(os.environ["REDIS_URL"], decode_responses=True)
@app.get("/products/<int:product_id>")
def get_product(product_id: int):
key = f"product:{product_id}"
if (cached := r.get(key)) is not None:
return {"source": "cache", "product": json.loads(cached)}
product = {"id": product_id}
r.set(key, json.dumps(product), ex=60)
return {"source": "origin", "product": product}
You provide REDIS_URL per environment and run Redis yourself locally. Flask runs each async view in its own event loop, which is why the example uses the synchronous client: a shared redis.asyncio client can fail with loop errors.
Flask-Caching caches whole responses with a decorator:
from flask import Flask
from flask_caching import Cache
app = Flask(__name__)
cache = Cache(app, config={"CACHE_TYPE": "RedisCache", "CACHE_REDIS_URL": "redis://localhost:6379/0"})
@app.get("/products/<int:product_id>")
@cache.cached(timeout=60)
def get_product(product_id: int):
return {"id": product_id}
Response caching suits pages that are the same for every user. The cache key is the request path by default, so personalized responses need make_cache_key or should not be cached this way.
Flask-Limiter adds rate limits as decorators and can store counters in Redis:
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(get_remote_address, app=app, storage_uri="redis://localhost:6379")
@app.post("/login")
@limiter.limit("5 per minute")
def login():
return {"ok": True}
Limits key on the client address by default, which counts every user behind one proxy together. Keying on the submitted email, as in the Encore example, needs a custom key function.
Python support in Encore is in beta, and you can sign up for access to try it with your Flask app.
Create one client at startup and use it from your views. With Encore, declare cache = redis.Database("cache"), then call await cache.string.get[str](key) and await cache.string.set(key, value, ex=60) from async Flask views. encore run starts Redis locally, and on deploy it becomes ElastiCache on AWS or Memorystore on GCP.
Keep a counter per user and window. Create the key with its expiry first using SET with NX and EX, then INCR it and reject the request when the count passes the limit. Creating the expiry first means the counter cannot outlive its window.
No. Flask-Caching adds decorators that cache whole view responses, which is convenient for pages. For caching individual objects or counting requests, a Redis client used directly gives you more control over keys and expiry.
Yes. Install Flask with the async extra and define views with async def. Each request then runs its coroutine, so awaitable Redis clients work inside the view.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.