Moleculer’s Built‑In Action Caching – How to Speed Up Your Microservices
Learn how Moleculer’s declarative caching works, how to wire it up with Redis, and what trade‑offs you need to consider. A step‑by‑step example shows caching a timestamp action and invalidation via events.
12 May 2026, 08:35 UTC

Problem: Expensive Actions and Unnecessary Load
In a microservice architecture you often have actions that hit databases, call external APIs, or perform heavy calculations. If those actions are invoked frequently with the same parameters, you’re repeatedly paying the same cost. A simple way to reduce latency and resource usage is to cache the result of the action.
Thesis: Moleculer’s Declarative Caching Feature
Moleculer offers a built‑in, declarative caching layer that sits between the transit layer and the action handler. By adding a cache option to an action, you tell the framework to look up a cached value before executing the handler. If a fresh entry exists, the cached result is returned immediately; otherwise the handler runs and the result is stored for future calls.
1. How the Cache Layer Works
The cache is a thin wrapper that runs automatically for every action marked with cache:
- When a request arrives, Moleculer generates a cache key. By default the key is a SHA‑256 hash of the action name and the JSON‑stringified parameters. You can override this with
keyorkeyGeneratorfunctions. - The framework queries the configured backend (memory, Redis, etc.) for that key.
- If a value exists and is younger than the TTL, Moleculer returns it and increments the
cache.hitsmetric. - Otherwise the action handler runs, the result is serialized, stored with the key and TTL, and the
cache.missesmetric is incremented.
Because the cache operates before the handler, no code changes are required in your business logic.
2. Configuring the Cache Backend
Cache backends are defined in the service’s cache property. The most common backend is Redis, which is shared across all service instances.
module.exports = {
name: "time-service",
actions: {
getCurrentTime: {
cache: {
ttl: 60, // seconds
store: "redis",
redis: {
host: "127.0.0.1",
port: 6379
}
},
async handler() {
return { ts: Date.now() };
}
}
}
};
Key points:
ttlis the maximum age of a cached entry. After it expires, the next call will re‑run the handler.- When using
store: "memory", the cache lives only in the current Node.js process. This is fine for single‑instance deployments but will lead to stale data in a cluster. - Redis supports
keyPrefixto avoid collisions if you run multiple services on the same instance.
3. Worked Example: Caching a Timestamp Action
Below is a minimal service that returns the current timestamp. With caching enabled, repeated calls within 30 seconds will return the same value.
// services/time.js
module.exports = {
name: "time",
actions: {
now: {
cache: {
ttl: 30,
store: "redis",
redis: {
host: "localhost",
port: 6379
}
},
async handler() {
return { ts: Date.now() };
}
}
}
};
Run the service with moleculer run services/time.js. Call the action via the API gateway:
curl http://localhost:3000/api/time.now
First call returns the current time. Subsequent calls within 30 s return the same timestamp. After 30 s, a new timestamp is generated.
To verify the cache hit/miss counters, enable the Prometheus plugin and inspect cache.hits and cache.misses metrics, or log them manually inside the handler.
4. Invalidating Cached Data
Sometimes you need to force a refresh (write‑through or write‑behind patterns). Moleculer exposes two mechanisms:
- Event‑based invalidation – broadcast a
cache.cleanevent with a key pattern. For example, to clear all keys for thenowaction:this.broker.broadcast("cache.clean", { pattern: "time.now" }); - Manual clean via
cleanAfter– specify a function that runs after the action completes and can delete specific keys:now: { cache: { ttl: 30, ... }, async handler() { /*...*/ }, cleanAfter(ctx) { const key = ctx.getCacheKey(); ctx.broker.cacher.remove(key); } }
Be careful: clearing keys too aggressively can negate the performance benefit. Use patterns that target only the affected data.
5. Trade‑Offs and Limitations
- Staleness in Multi‑Instance Deployments – If you use the default memory store, each instance maintains its own cache. A write in one instance won’t invalidate the cache in others. Always use a shared backend like Redis for cluster‑wide consistency.
- Memory Consumption – With very large TTLs or broad cache keys, you can exhaust the chosen backend. Monitor memory usage and adjust TTL or key granularity.
- Cache Penetration – If an action frequently returns
nullor throws errors, the framework may cache those failures unless you setcache: { ttl: 0 }for error paths. - Serialization Overhead – Cached values are serialized by the transit serializer (JSON, Avro, MsgPack). For binary data, consider using
msgpackto reduce size.
Actionable Closing
To decide whether to enable caching:
- Identify actions that are idempotent and heavy to compute.
- Choose a TTL that balances freshness and performance.
- Configure a shared backend (Redis recommended).
- Implement targeted invalidation for write operations.
- Instrument metrics to confirm hit/miss ratios.
Once set up, you’ll see lower latency and CPU usage, and your services will scale more gracefully under load.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.