Leveraging Moleculer’s Built‑In Action Cache for Low‑Latency Reads
Use Moleculer’s built‑in action cache to serve repeat reads from memory, reduce latency, and avoid external caching layers. Learn how to configure cache, invalidate via events, and handle the trade‑offs in a microservice environment.
23 Apr 2026, 00:16 UTC

Problem: Repeated Expensive Reads in a Microservice Architecture
In a typical microservice stack, a service often needs to fetch the same data repeatedly – for example, user profiles or product catalogs. If each request hits a database or external API, the latency compounds, and the database can become a bottleneck. Adding a separate caching layer (Redis, Memcached, etc.) introduces more moving parts and network hops. Moleculer, a popular Node.js microservices framework, offers a simpler alternative: per‑action in‑memory caching that can be enabled with a few configuration options.
Thesis: Enable Moleculer’s Action Cache to Reduce Latency Without Extra Infrastructure
By declaring a cache in the action settings, you can serve repeated requests from memory. The framework handles key generation, TTL, and provides hooks for cache invalidation. This pattern is ideal for read‑heavy services that mutate data in a controlled way.
Section 1 – Declaring a Cached Action
Below is a minimal Moleculer service that exposes a user.get action. The action is configured with caching enabled, a TTL of 30 seconds, and a custom key generator that uses the userId parameter.
module.exports = {
name: "user-service",
actions: {
get: {
cache: {
enabled: true,
ttl: 30000, // 30 seconds in ms
key: (ctx) => `user:${ctx.params.userId}`
},
async handler(ctx) {
// Simulate expensive DB call
const user = await db.findUserById(ctx.params.userId);
return user;
}
}
}
};
Key points:
- enabled turns the cache on for this action only.
- ttl is the time‑to‑live in milliseconds; after this period the cached entry expires automatically.
- key is a function that receives the action context. The returned string is used as the cache key. It must be deterministic and cover all request parameters that affect the result.
Section 2 – Cache Invalidation via Events
Because the cache lives only in memory, it can become stale if data changes. Moleculer encourages a publish‑subscribe pattern: when a service mutates data, it publishes a domain event. Other services that rely on cached data listen for this event and clear the relevant cache entry.
module.exports = {
name: "user-service",
events: {
"user.updated"(payload) {
// Invalidate cache for the updated user
this.broker.cacher.del(`user:${payload.userId}`);
}
},
actions: {
update: {
async handler(ctx) {
const updated = await db.updateUser(ctx.params);
// Notify other services
this.broker.broadcast("user.updated", { userId: ctx.params.id });
return updated;
}
}
}
};
When user.update runs, it broadcasts user.updated. The get action’s service receives the event and removes the stale key. The next user.get call will hit the database again, ensuring eventual consistency.
Section 3 – Practical Diagnostics and Verification
To confirm the cache behaves as expected, you can add a simple counter in the handler:
let execCount = 0;
async handler(ctx) {
execCount++;
console.log(`Handler executed ${execCount} time(s)`);
...
}
Then run the following sequence from a Node REPL or another service:
await broker.call("user.get", { userId: 42 }); // First call – handler runs
await broker.call("user.get", { userId: 42 }); // Second call – should be a cache hit
await broker.call("user.update", { id: 42, name: "Alice" }); // Publishes event
await broker.call("user.get", { userId: 42 }); // Cache miss – handler runs again
Check the console logs or, if you have enabled Moleculer metrics, inspect the cacher.hits and cacher.misses counters.
Trade‑Offs & Limitations
- Per‑Node Cache: The cache is local to each broker instance. In a horizontally scaled deployment, each node holds its own copy, which can increase memory usage and lead to cache misses until all nodes warm up.
- Deterministic Keys: If the key function omits a parameter that influences the result, the cache may return stale or incorrect data. Always include all relevant inputs.
- TTL vs Invalidation: Setting a long TTL reduces load but risks serving stale data. Coupling TTL with explicit invalidation events gives you fine control but requires disciplined event publishing.
- Version Sensitivity: Cache options and behavior can change between Moleculer major releases. Verify the documentation for the specific version you run.
Actionable Take‑Away
1. Add cache settings to your read‑heavy actions, tailoring ttl and key to your data model.
2. Publish domain events whenever you mutate the underlying data and listen for those events in services that consume cached data.
3. Monitor cache hit/miss ratios using Moleculer’s built‑in metrics or a custom logger. Adjust TTL or key logic as needed.
4. In a clustered environment, consider a distributed caching layer (Redis, Memcached) if you need shared cache state across nodes, but remember the added complexity.
By following this pattern, you can dramatically lower read latency while keeping your architecture lightweight and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.