Skip to main content

Overview

Budget Bee uses Redis as an optional caching layer to improve performance for session storage, rate limiting, and frequently accessed data. Redis is implemented via the ioredis library.
Redis is optional for Budget Bee. The application will work without it, but performance may be reduced for high-traffic deployments.

Redis Implementation

The Redis client is configured in packages/core/redis.ts:
import { Redis } from "ioredis";

const globalForRedis = global as unknown as { redis: Redis };

const getRedisUrl = () => {
  if (!process.env.REDIS_URL) throw new Error("REDIS_URL is not set");
  return process.env.REDIS_URL;
}

export const redis = globalForRedis.redis || new Redis(getRedisUrl());

if (process.env.NODE_ENV !== "production") globalForRedis.redis = redis;
In development, Redis uses a singleton pattern to avoid creating multiple connections during hot reloads.

Environment Configuration

Add Redis configuration to your .env file:
# Redis Configuration
REDIS_URL=redis://localhost:6379

# Optional: Redis with authentication
REDIS_URL=redis://:password@localhost:6379

# Optional: Redis cluster or cloud (e.g., Upstash)
REDIS_URL=rediss://default:[email protected]:6380

Docker Setup

Add Redis to your Docker Compose configuration:

Create infra/bu-redis.yml

services:
  bu-redis:
    image: redis:7-alpine
    container_name: bu-redis
    restart: unless-stopped
    networks:
      - bu-net
    ports:
      - 6379:6379
    volumes:
      - bu-redis-data:/data
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-}
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD:-}

Update infra/docker-compose.yml

include:
  - bu-postgres.yml
  - bu-postgrest.yml
  - bu-adminer.yml
  - bu-redis.yml  # Add this line

volumes:
  bu-postgres-data:
    name: bu-postgres-data
  bu-redis-data:  # Add this
    name: bu-redis-data

networks:
  bu-net:
    name: bu-net
    driver: bridge

Start Redis

cd infra && docker compose up -d bu-redis

Use Cases

Session Storage

Redis can store user sessions for Better Auth:
import { redis } from "@/lib/redis";

// Store session
await redis.setex(
  `session:${sessionToken}`,
  3600, // 1 hour TTL
  JSON.stringify(sessionData)
);

// Retrieve session
const sessionData = await redis.get(`session:${sessionToken}`);

// Delete session
await redis.del(`session:${sessionToken}`);

Rate Limiting

Implement API rate limiting:
import { redis } from "@/lib/redis";

const rateLimit = async (userId: string, limit: number = 100) => {
  const key = `rate_limit:${userId}`;
  const current = await redis.incr(key);
  
  if (current === 1) {
    // First request, set expiry
    await redis.expire(key, 60); // 1 minute window
  }
  
  if (current > limit) {
    throw new Error("Rate limit exceeded");
  }
  
  return { remaining: limit - current };
};

Caching Expensive Queries

Cache transaction aggregations:
import { redis } from "@/lib/redis";

const getCachedTransactionStats = async (userId: string, orgId: string | null) => {
  const cacheKey = `stats:${orgId || userId}`;
  
  // Try cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // Cache miss - fetch from database
  const stats = await db.query(/* expensive query */);
  
  // Cache for 5 minutes
  await redis.setex(cacheKey, 300, JSON.stringify(stats));
  
  return stats;
};

Invalidating Cache

import { redis } from "@/lib/redis";

// After creating/updating transaction
await redis.del(`stats:${orgId || userId}`);

// Pattern-based deletion
const keys = await redis.keys(`stats:${orgId}:*`);
if (keys.length > 0) {
  await redis.del(...keys);
}

Redis Commands

String Operations

// Set key-value
await redis.set("key", "value");

// Set with expiry (seconds)
await redis.setex("key", 3600, "value");

// Get value
const value = await redis.get("key");

// Delete key
await redis.del("key");

// Increment
await redis.incr("counter");
await redis.incrby("counter", 5);

Hash Operations

// Store object as hash
await redis.hset("user:123", "name", "John Doe");
await redis.hset("user:123", "email", "[email protected]");

// Get hash field
const name = await redis.hget("user:123", "name");

// Get all fields
const user = await redis.hgetall("user:123");

// Delete hash field
await redis.hdel("user:123", "email");

Set Operations

// Add to set
await redis.sadd("active_users", "user_123", "user_456");

// Get all members
const users = await redis.smembers("active_users");

// Check membership
const isMember = await redis.sismember("active_users", "user_123");

// Remove from set
await redis.srem("active_users", "user_123");

Sorted Sets (Leaderboards)

// Add with score
await redis.zadd("leaderboard", 100, "user_123");
await redis.zadd("leaderboard", 150, "user_456");

// Get top 10
const top10 = await redis.zrevrange("leaderboard", 0, 9, "WITHSCORES");

// Get user rank
const rank = await redis.zrevrank("leaderboard", "user_123");

List Operations

// Push to list
await redis.lpush("notifications", JSON.stringify(notification));

// Get list range
const notifications = await redis.lrange("notifications", 0, 9);

// Pop from list
const latest = await redis.lpop("notifications");

Pub/Sub for Real-Time Updates

Publisher

import { redis } from "@/lib/redis";

// Publish transaction update
await redis.publish(
  "transactions:updates",
  JSON.stringify({
    type: "created",
    transactionId: "tx_123",
    userId: "user_456"
  })
);

Subscriber

import { Redis } from "ioredis";

const subscriber = new Redis(process.env.REDIS_URL!);

subscriber.subscribe("transactions:updates");

subscriber.on("message", (channel, message) => {
  const update = JSON.parse(message);
  console.log(`New transaction: ${update.transactionId}`);
  
  // Invalidate cache, send websocket update, etc.
});

Production Configuration

Redis with Password

# .env
REDIS_URL=redis://:your_secure_password@localhost:6379
# bu-redis.yml
command: redis-server --appendonly yes --requirepass your_secure_password

Redis Cluster (High Availability)

For production, use a managed Redis service or cluster:
import { Cluster } from "ioredis";

const redis = new Cluster([
  { host: "redis-1.example.com", port: 6379 },
  { host: "redis-2.example.com", port: 6379 },
  { host: "redis-3.example.com", port: 6379 },
], {
  redisOptions: {
    password: process.env.REDIS_PASSWORD,
  },
});

Managed Redis Services

Upstash

Serverless Redis with global edge caching

Redis Cloud

Official Redis managed service

AWS ElastiCache

Fully managed Redis on AWS

Upstash Example

REDIS_URL=rediss://default:[email protected]:6380

Monitoring

Check Redis Connection

import { redis } from "@/lib/redis";

try {
  await redis.ping();
  console.log("Redis connected!");
} catch (error) {
  console.error("Redis connection failed:", error);
}

Monitor Redis Stats

# Via CLI
docker exec bu-redis redis-cli INFO stats

# Via Node.js
const info = await redis.info("stats");
console.log(info);

Key Metrics

# Connected clients
docker exec bu-redis redis-cli CLIENT LIST

# Memory usage
docker exec bu-redis redis-cli INFO memory

# Slow queries
docker exec bu-redis redis-cli SLOWLOG GET 10

Performance Optimization

Connection Pooling

const redis = new Redis(process.env.REDIS_URL!, {
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
  lazyConnect: false,
});

Pipelining

Batch multiple commands:
const pipeline = redis.pipeline();

pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.incr("counter");

const results = await pipeline.exec();

Lua Scripts (Atomic Operations)

const script = `
  local current = redis.call('GET', KEYS[1])
  if tonumber(current) < tonumber(ARGV[1]) then
    redis.call('INCR', KEYS[1])
    return 1
  else
    return 0
  end
`;

const result = await redis.eval(script, 1, "rate_limit:user_123", "100");

Backup and Persistence

Enable Persistence

# bu-redis.yml
command: redis-server --appendonly yes --save 60 1000
# Saves if 1000 keys changed in 60 seconds

Manual Backup

# Create snapshot
docker exec bu-redis redis-cli BGSAVE

# Copy snapshot
docker cp bu-redis:/data/dump.rdb ./backup/redis-dump.rdb

Restore from Backup

# Stop Redis
docker stop bu-redis

# Replace snapshot
docker cp ./backup/redis-dump.rdb bu-redis:/data/dump.rdb

# Start Redis
docker start bu-redis

Security Best Practices

Use Strong Passwords

Always set requirepass in production

Disable Dangerous Commands

Rename FLUSHALL, FLUSHDB, CONFIG

Network Isolation

Run Redis in private Docker network

Use TLS

Enable SSL for Redis connections

Disable Dangerous Commands

command: >
  redis-server
  --requirepass your_password
  --rename-command FLUSHALL ""
  --rename-command FLUSHDB ""
  --rename-command CONFIG ""

Troubleshooting

Connection Errors

Error: REDIS_URL is not set
Solution: Add REDIS_URL to .env:
REDIS_URL=redis://localhost:6379

Authentication Errors

Error: NOAUTH Authentication required
Solution: Include password in URL:
REDIS_URL=redis://:your_password@localhost:6379

Memory Issues

Error: OOM command not allowed when used memory > 'maxmemory'
Solution: Set eviction policy:
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

Alternative: Running Without Redis

If you don’t want to use Redis, ensure your application handles missing Redis gracefully:
// Optional Redis client
let redis: Redis | null = null;

try {
  if (process.env.REDIS_URL) {
    redis = new Redis(process.env.REDIS_URL);
  }
} catch (error) {
  console.warn("Redis not available, using in-memory cache");
}

export const getCache = async (key: string) => {
  if (!redis) return null;
  return await redis.get(key);
};

Next Steps

PostgreSQL Config

Optimize database settings

PostgREST Setup

Configure REST API

Docker Deployment

Deploy with Docker

Build docs developers (and LLMs) love