In-memory cache
InMemoryCache is the default Cache implementation. It’s a
Map with three things layered on top: LRU eviction bounded by
maxEntries, lazy per-entry TTL, and an optional background
sweep that reclaims expired-but-untouched entries. In-process, zero
dependencies, lost on restart.
import { InMemoryCache, InMemoryCacheOptions } from 'actor-ts';
// Defaults: maxEntries 10 000, cleanupMs 60 000.const cache = new InMemoryCache();
// Or tune it with the builder:const cacheOptions = InMemoryCacheOptions.create() .withMaxEntries(50_000) .withCleanupMs(30_000);const tuned = new InMemoryCache(cacheOptions);A plain object is the shorthand alternative —
new InMemoryCache({ maxEntries: 50_000, cleanupMs: 30_000 }). A TTL
is still supplied per call (see TTL handling), not
at construction time.
Options
Section titled “Options”| Field | Builder | Default | Meaning |
|---|---|---|---|
maxEntries | withMaxEntries(n) | 10000 | LRU cap on stored entries. Infinity = unbounded. |
cleanupMs | withCleanupMs(ms) | 60000 | Background expired-entry sweep interval (ms). 0 / Infinity disables the sweep. |
Values are validated once, at construction, on the merged settings:
maxEntries must be a positive integer (or Infinity) and cleanupMs
a non-negative number (or Infinity). A bad value throws
OptionsError — the
builder, a plain object, and HOCON all hit the same check.
When to use it
Section titled “When to use it”Three scenarios:
- Tests — fast, no IO, clean teardown via
close(). - Single-process production — one process, no need to share cache state across pods.
- Dev / local — the same code without Redis on the laptop.
For multi-process deployments, use Redis or Memcached instead — each process would otherwise keep its own independent copy.
The operations
Section titled “The operations”InMemoryCache implements the full Cache
surface:
await cache.set('user:1', user, 60_000); // value + optional TTL (ms)const hit = await cache.get<User>('user:1'); // Option<User> — None on miss/expiry
await cache.setIfAbsent('lock:job', '1', 30_000); // true iff it was storedawait cache.incr('ratelimit:1.2.3.4', 60_000); // atomic ++, returns the new count
const many = await cache.mget<User>(['user:1', 'user:2']); // Map<string, User>await cache.mset(new Map([['a', user1], ['b', user2]]), 60_000);
await cache.delete('user:1', 'user:2'); // one or many keysawait cache.close(); // stops the sweep + clears the MapsetIfAbsent is atomic here for a structural reason rather than a
protocol one: its read and write sit in the same synchronous block,
and the single-threaded event loop cannot interleave another caller
between them. That holds within one process only — two Node
processes each have their own Map, so a lock:job key guards
nothing across them. For cross-process locking, use
acquireLock over RedisCache.
get returns an Option<V> — None on a miss or after expiry.
TTL handling
Section titled “TTL handling”await cache.set('key', value, 60_000); // expires at now + 60sawait cache.set('key', value); // no TTL — lives until evicted/deletedExpiry has two paths. It is lazy on access — an entry’s deadline
is checked on every get, mget, incr, and setIfAbsent, and an
expired entry is dropped at that point. A background sweep then
runs every cleanupMs (default 60 s) to reclaim expired entries that
are never touched again, so they don’t sit in the Map waiting for a
read. Set cleanupMs to 0 / Infinity to turn the sweep off and
rely on lazy expiry alone.
incr sets the TTL only when it creates the counter (the value
becomes 1); later increments don’t refresh it — the right
semantics for a fixed-window rate limiter.
Bounded by default (LRU)
Section titled “Bounded by default (LRU)”InMemoryCache is LRU-bounded at maxEntries (default 10 000):
inserting a new key beyond the cap evicts the least-recently-used
entry. get/incr/setIfAbsent count as a use and move a key to the
most-recently-used end, so hot keys survive and cold ones are evicted
first. This is what keeps a flood of distinct, never-re-read keys —
attacker-chosen Idempotency-Key or rate-limit keys, for instance —
from growing the map without limit.
Set maxEntries: Infinity to opt out of eviction entirely. Only do
this when you control the key space — an unbounded map OOMs the process
eventually.
Sharing across the system
Section titled “Sharing across the system”import { CacheExtensionId, InMemoryCache } from 'actor-ts';
// The extension's `default` cache is an InMemoryCache out of the box:const cache = system.extension(CacheExtensionId).cache();
// Override the default, or register a separate named cache:system.extension(CacheExtensionId).setCache('default', new InMemoryCache());const sessions = system.extension(CacheExtensionId).cache('sessions');system.extension(CacheExtensionId).cache(name) resolves a cache
by name — the default name is an InMemoryCache unless you
replace it (via setCache, registerCache, or the HOCON path
actor-ts.cache.<name>.plugin). HTTP middleware, projection
actors, and your own code then share one configured instance
instead of each building its own.
The in-memory cache built by the extension reads its defaults from HOCON:
actor-ts.cache.in-memory { maxEntries = 50000 # LRU cap cleanupMs = 30000 # background sweep interval, ms (0 disables)}For a throwaway cache, just construct new InMemoryCache()
directly — no extension needed.
When it’s wrong for production
Section titled “When it’s wrong for production”Where to next
Section titled “Where to next”- Cache overview — the bigger picture.
- Redis cache — multi-process alternative.
- Memcached cache — alternative.
- CachedSnapshotStore — one consumer of the cache abstraction.
