What you'll actually use Redis for
Redis is rarely "just a cache." In a typical app it ends up doing three or four jobs: caching expensive queries, holding sessions, backing a rate limiter, and serving as a pub/sub backplane or job queue. This tutorial connects your app to a managed Redis and shows the patterns that matter, in Node and Python.
Assumption: you have a managed Redis and a REDIS_URL in the environment. On PandaStack, provisioning Redis (via KubeBlocks) injects the connection string automatically.
The connection string
redis://:PASSWORD@HOST:6379
rediss://:PASSWORD@HOST:6379 # note the double-s: TLSManaged Redis usually uses rediss:// (TLS). Read it from the env; never hardcode.
Node.js (ioredis)
ioredis handles reconnection, clustering, and TLS cleanly:
import Redis from "ioredis";
export const redis = new Redis(process.env.REDIS_URL, {
maxRetriesPerRequest: 3,
enableReadyCheck: true,
// rediss:// in the URL enables TLS automatically
});
redis.on("error", (e) => console.error("redis error", e));Create the client once and reuse it — don't instantiate per request. ioredis multiplexes commands over a single connection, so one client handles high throughput.
Python (redis-py)
import os
import redis
pool = redis.ConnectionPool.from_url(
os.environ["REDIS_URL"],
max_connections=20,
decode_responses=True,
)
r = redis.Redis(connection_pool=pool)The connection pool reuses connections across requests; decode_responses=True saves you manual byte decoding.
Pattern 1: cache-aside
The most common pattern — check cache, fall back to the source, populate the cache:
async function getProduct(id) {
const key = `product:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const product = await db.fetchProduct(id); // slow source
// expire after 5 minutes so stale data self-heals
await redis.set(key, JSON.stringify(product), "EX", 300);
return product;
}Always set a TTL (EX). Cache entries without expiry are how you get mysterious stale data and unbounded memory growth.
Pattern 2: rate limiting
A simple fixed-window limiter with INCR + EXPIRE:
async function allow(ip, limit = 100, windowSec = 60) {
const key = `rate:${ip}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count <= limit;
}For production, a sliding-window or token-bucket via a Lua script avoids the fixed-window burst edge, but the above is fine to start.
Pattern 3: sessions
Storing sessions in Redis lets multiple app instances share login state — essential once you scale past one instance:
import session from "express-session";
import { RedisStore } from "connect-redis";
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 86_400_000 },
}));Pattern 4: pub/sub
For fan-out across instances (e.g., a WebSocket backplane), use separate clients for publishing and subscribing — a subscribed connection can't run normal commands:
const sub = redis.duplicate();
await sub.subscribe("events");
sub.on("message", (channel, message) => handle(JSON.parse(message)));
// elsewhere
await redis.publish("events", JSON.stringify({ type: "deploy.done" }));TLS gotchas
- Use
rediss://(double-s) for TLS — the most common cause of "connection reset" against managed Redis is usingredis://when TLS is required. - If the provider uses a self-signed/private CA and your client rejects it, you may need to relax cert verification or supply the CA, similar to Postgres.
Memory and eviction
Managed Redis has a memory cap. Know your eviction policy:
noeviction— writes fail when full (safe for queues, dangerous for pure caches).allkeys-lru— evicts least-recently-used keys (good for caches).
If you're using Redis purely as a cache, allkeys-lru plus TTLs keeps memory bounded. If it's also a queue or session store, be careful — you don't want eviction silently dropping jobs. Separate concerns into different Redis instances/databases when in doubt.
Deploying on PandaStack
- 1Provision a managed Redis from the dashboard.
- 2Attach it to your service —
REDIS_URLis injected automatically (TLS-enabledrediss://). - 3Create the client once at startup and reuse it.
- 4Deploy; confirm connectivity in the live logs.
Checklist
- One client/pool, reused — never per request.
- TLS via
rediss://. - TTL on every cache key.
- Separate clients for pub/sub.
- Eviction policy matches the use case.
- Don't mix a job queue and a volatile cache in the same instance with
allkeys-lru.
References
- ioredis: https://github.com/redis/ioredis
- redis-py: https://redis.readthedocs.io/en/stable/
- Redis key expiration: https://redis.io/docs/latest/develop/use/keyspace/#key-expiration
- Redis eviction policies: https://redis.io/docs/latest/operate/oss_and_stack/management/config-file/#maxmemory-policy
- connect-redis session store: https://github.com/tj/connect-redis
---
Need managed Redis with the URL wired straight into your app? PandaStack provisions Redis via KubeBlocks and injects REDIS_URL automatically. Try it free at https://dashboard.pandastack.io