Skip to main content

Patterns

Redis cache strategies (balance, stats, webhooks)

Short TTL + event-driven invalidation on webhooks

Redis is used at Scell.io for 4 cases. (1) Tenant balance cache: balance changes after each invoice/reload, short TTL (60s) + immediate invalidation on `balance.updated` webhook. (2) Aggregated stats cache: monthly invoice count, VAT totals, top clients; expensive computation, 5-min TTL, invalidation on `invoice.created` / `invoice.cancelled`. (3) Idempotency keys (24h TTL, see dedicated pattern). (4) Rate limiting: sliding window with `INCR + EXPIRE`. Critical pattern: NEVER cache fiscal data without the corresponding invalidation chain. A balance cache serving a stale amount causes double-debits or transaction refusals. The `cache → webhook → invalidate` rule must be documented and tested. Anti-pattern: write-through cache to Redis from the application (inconsistency if service crashes between DB and Redis); prefer cache-aside (read cache miss → DB → cache write) with event-driven invalidation on webhook.

Key facts

  • Tenant balance: 60s TTL + invalidation on balance.updated webhook
  • Aggregated stats: 5-min TTL + invalidation on invoice.created/cancelled
  • Idempotency keys: 24h TTL
  • Rate limit: sliding window via INCR + EXPIRE
  • Pattern: cache-aside, never write-through for fiscal data

Code example

import { createClient } from 'redis';
const redis = createClient();

// Cache-aside avec invalidation event-driven
async function getBalance(tenantId: string): Promise<number> {
  const key = `balance:${tenantId}`;
  const cached = await redis.get(key);
  if (cached !== null) return Number(cached);
  const fresh = await db.query('SELECT balance FROM tenants WHERE id = $1', [tenantId]);
  await redis.set(key, String(fresh), { EX: 60 }); // 60s TTL safety net
  return fresh;
}

// Webhook handler — invalidation immédiate
async function onBalanceUpdated(event: { tenant_id: string }) {
  await redis.del(`balance:${event.tenant_id}`);
  await redis.del(`stats:${event.tenant_id}:current_month`);
}

// Rate limit sliding window
async function checkRateLimit(apiKey: string, limit = 1000, windowSec = 60): Promise<boolean> {
  const key = `rl:${apiKey}:${Math.floor(Date.now() / 1000 / windowSec)}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, windowSec);
  return count <= limit;
}

See also

Your cookie preferences

We use cookies to improve your experience. Essential cookies are always active. Cookie policy.