Cutting Database Load in Moleculer with Redis Action Caching
Learn how to add Redis-backed action caching to Moleculer microservices, reduce database load, and handle staleness and Redis outages without guesswork.
01 Sept 2026, 23:53 UTC

The repeated-query problem
Most microservices eventually expose an endpoint that gets called over and over with the same arguments. A getUser action hit by a dashboard, a mobile app, and an internal batch job will issue the same database query dozens of times per minute. Moleculer's built-in action caching gives you a declarative way to short-circuit that work: attach a cache option to the action, and the broker will store the result in a shared backend such as Redis.
How the cache option works
In Moleculer, caching is configured per action, not per service. You add a cache property to the action definition. Setting it to true enables caching with defaults. More commonly, you pass an object so you can control the time-to-live (ttl) and which parameters form the cache key.
The keys array is the part engineers most often overlook. If you omit it, Moleculer builds the cache key from all request parameters. That is usually fine for a single-ID lookup, but it can explode if your action accepts filter objects, pagination cursors, or optional flags. Listing the exact parameter names that should vary the cache entry keeps the key space small and predictable.
Wiring Redis into the broker
The cache backend is configured at the broker level. Moleculer ships with an in-memory cacher by default, which is useful for a single instance but useless once you run two or more replicas—each process would hold its own copy. To share cache entries across instances, switch the broker to the built-in Redis cacher. This requires the redis package to be installed in your project.
Add the following to your broker startup file (for example, moleculer.config.js). You need Node.js installed and a reachable Redis server. Replace localhost and 6379 with your actual Redis host and port.
// moleculer.config.js
module.exports = {
nodeID: "user-service",
cacher: {
type: "Redis",
options: {
redis: {
host: "localhost",
port: 6379
}
}
}
};
If Redis is unreachable when the broker starts, the process will fail to connect. If Redis becomes unavailable later, Moleculer logs a warning and falls back to executing the action directly. That fallback prevents hard failures, but it also means a Redis outage turns into a sudden spike of database queries. Monitor your Redis instance and consider a fallback strategy—such as a short in-memory TTL—if your traffic is high.
A worked example: caching a user lookup
Imagine a users service with a get action that queries a database. Without caching, every call hits the DB. With Redis caching, repeated calls for the same user ID return the stored result until the TTL expires.
Create or edit services/users.service.js:
module.exports = {
name: "users",
actions: {
get: {
cache: {
keys: ["id"],
ttl: 5 // seconds
},
async handler(ctx) {
// Log whether this invocation came from cache
if (ctx.meta.$cache) {
this.logger.info("Cache hit for user", ctx.params.id);
}
// Replace with your real DB call
const user = await ctx.call("$db.findUser", { id: ctx.params.id });
return user;
}
}
}
};
To verify the behavior locally, start your broker and a Redis server, then invoke the action twice with identical parameters using the Moleculer CLI or an API gateway. The first call executes the handler; the second call should return the same payload, and the ctx.meta.$cache flag inside the handler should be truthy, confirming a cache hit. After the 5-second TTL elapses, the next call executes the handler again and refreshes the entry.
Staleness, TTLs, and invalidation
The trade-off is staleness. A cached user record will not reflect a database update until the TTL expires or the entry is evicted. A 5-second TTL is a safe starting point for read-heavy services; a 5-minute TTL may be appropriate for rarely changing reference data, but it will serve outdated information for that entire window.
If your service writes data, you should invalidate the cache explicitly. Moleculer's cacher interface exposes methods to remove entries, but the exact key format is internal. A pragmatic approach is to keep TTLs short enough that manual invalidation is unnecessary for most actions, and reserve explicit clearing for the few cases where a write must be immediately visible.
Memory and payload size
Redis is fast, but it is not free. Caching large objects—such as full user profiles with embedded permissions or long lists—consumes memory proportional to your traffic. A long TTL multiplies that cost because entries linger. Keep cached payloads lean: cache the minimal shape the caller needs, and avoid storing megabyte-sized responses unless you have provisioned Redis accordingly.
A practical way to check the result
Before rolling this out to production, add temporary logging inside your action to count cache hits versus handler executions. Call the endpoint under realistic concurrency and confirm that the hit rate rises while database query counts fall. If the hit rate stays near zero, your keys array is probably too narrow or your TTL too short. If Redis memory climbs steadily, audit the size of the objects you are returning.
Start with a conservative TTL, instrument the hit rate, and expand caching only to actions where the read pattern justifies it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.