Skip to main content

Caching Overview

Balda ships with a first-class, provider-agnostic HTTP caching layer. When enabled, it transparently intercepts route handlers, stores responses in your chosen backend, and serves subsequent identical requests directly from cache — without executing the handler again.

Features

  • Zero-config in-memory cache — works out of the box, no dependencies
  • Redis backend — persistent, shared cache across multiple instances (ioredis)
  • Custom providers — bring any storage backend via the CacheProvider interface
  • Fine-grained key control — include body, query, headers, or a custom discriminator in the cache key
  • Tag-based invalidation — group entries by tag and invalidate them in one call
  • Thundering herd protection — distributed locks prevent cache stampedes
  • Response compression — gzip large responses before storing
  • Statistics — track hit rate, miss count, and invalidations via getCacheService()?.getStats()

Quick Start

There are two ways to enable caching. Use whichever fits your setup:

Option 1 — via plugins.cache in ServerOptions:

import { Server } from 'balda';

const server = new Server({
port: 3000,
plugins: {
cache: {
provider: 'memory', // or 'redis', or a custom CacheProvider instance
defaultTtl: 300,
},
},
});

Option 2 — via server.use() with cacheMiddleware():

import { Server, cacheMiddleware, MemoryCacheProvider } from 'balda';

const server = new Server({ port: 3000 });
server.use(cacheMiddleware(new MemoryCacheProvider(), { defaultTtl: 300 }));

Both approaches initialize the global CacheService so that @cache() decorators and getCacheService() work everywhere. Once initialized, use the @cache() decorator or inline router config on any route:

import { controller, get, cache } from 'balda';

@controller('/api/products')
class ProductController {
@get('/')
@cache({ ttl: 60 })
async list(_req, res) {
res.json(await db.products.findAll());
}
}

Configuration

The cache service is standalone. Configure it via plugins.cache in ServerOptions, server.use(cacheMiddleware(...)), or call initCacheService() directly.

OptionTypeDefaultDescription
defaultTtlnumber300Default TTL in seconds (max 86400)
compressionThresholdnumber1024Min response size (bytes) before gzip compression
keyPrefixstring'cache'Prefix applied to all cache keys
enableStatsbooleantrueTrack hit/miss statistics
lockTimeoutnumber5000Lock TTL in ms for thundering herd protection
lockBehavior'wait' | 'bypass' | 'fail''wait'What to do when a lock can't be acquired

Using plugins.cache in ServerOptions

import { Server } from 'balda';

const server = new Server({
port: 3000,
plugins: {
// Memory provider (default)
cache: { provider: 'memory', defaultTtl: 300 },

// Redis provider
// cache: { provider: 'redis', redis: { host: 'localhost' }, defaultTtl: 600 },

// Custom provider instance
// cache: { provider: new MyCustomProvider(), defaultTtl: 300 },
},
});

Using cacheMiddleware() with server.use()

Useful when you need more control over middleware ordering or want to compose the cache setup outside of server options:

import { Server, cacheMiddleware, MemoryCacheProvider } from 'balda';

const server = new Server({ port: 3000 });
server.use(cacheMiddleware(new MemoryCacheProvider(), { defaultTtl: 300 }));

Using initCacheService() Directly

For advanced use cases (e.g. CLI commands, workers) where you need the cache service without a server:

import { initCacheService, MemoryCacheProvider, DEFAULT_CACHE_OPTIONS } from 'balda';

initCacheService(new MemoryCacheProvider(), {
...DEFAULT_CACHE_OPTIONS,
defaultTtl: 300,
});

The x-cache Response Header

Every response from a cached route includes an x-cache header indicating the cache status:

ValueMeaning
MISSCache miss — handler executed, response stored
HITCache hit — response served from cache
WAITRequest waited for another in-flight request to populate the cache
BYPASSLock could not be acquired and lockBehavior was 'bypass'

Accessing the Cache Service

After calling initCacheService(), use getCacheService() to access the full CacheService API:

import { getCacheService } from 'balda';

// Invalidate by tags
await getCacheService()?.invalidate(['products']);

// Invalidate a specific key
await getCacheService()?.invalidateKey('cache:GET:/api/products::abc123');

// Get statistics
const stats = getCacheService()?.getStats();
console.log(`Hit rate: ${(stats.hitRate * 100).toFixed(1)}%`);
warning

getCacheService() returns null if initCacheService() has not been called. Always initialize the cache service before accessing it.