Oct 27, 20266 min read

How to Add Redis to a Flask App

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:

  • Whether you cache objects or whole responses.
  • How keys expire, so the cache cannot grow forever.
  • Where Redis runs locally and in production.
  • What happens when Redis is unavailable.

Redis options for Flask at a glance

CapabilityEncoreredis-pyFlask-CachingFlask-Limiter
UseCaching, counters, any key typeAny Redis commandCached view responsesRate limits on routes
Client setupredis.Database("cache")Redis.from_url(...)CACHE_REDIS_URLstorage_uri
Local Redisencore run starts itRun your ownRun your ownRun your own or in memory
ProductionElastiCache or Memorystore in your accountAny hosted RedisAny hosted RedisAny hosted Redis
Async viewsAwaitable APIredis.asyncioSynchronousSynchronous

Encore: Redis for your existing Flask app

Encore

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.

Encore declares infrastructure in Python code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in Python, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

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:

~/shop
$ encore run$ curl localhost:4000/products/7{"product": {...}, "source": "origin"}$ curl localhost:4000/products/7{"product": {...}, "source": "cache"}

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.

Good to know

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.

Try Encore →

redis-py

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}

Good to know

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

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}

Good to know

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

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}

Good to know

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.

How to choose

  • Encore if you want Redis running locally and as ElastiCache or Memorystore in your own cloud account from the same code.
  • redis-py for direct control in an app that already has a Redis URL per environment.
  • Flask-Caching to cache whole public pages with a decorator.
  • Flask-Limiter for route rate limits keyed on IP address.

Python support in Encore is in beta, and you can sign up for access to try it with your Flask app.

Frequently asked questions

How do I add Redis to a 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.

How do I rate limit a Flask route with Redis?

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.

Do I need Flask-Caching to use Redis in Flask?

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.

Can Flask views be async when using Redis?

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.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders