Moleculer Service Discovery and Load Balancing with NATS
Learn how Moleculer’s Registry automatically finds services and distributes calls using a NATS transporter, plus verification steps and known limits.
09 Feb 2026, 02:44 UTC

Problem: manually wiring service endpoints
When building a microservice system with Moleculer, hard‑coding host‑port pairs for each service call creates fragile configuration. Any change in deployment topology requires updating every caller, and you lose the ability to scale instances without editing code.
Thesis
Moleculer’s built‑in Registry, paired with a transporter such as NATS, provides zero‑config service discovery and automatic load balancing. By configuring a shared transporter and keeping service names/versions consistent, nodes advertise themselves and discover peers, while the LoadBalancer distributes calls using strategies like RoundRobin or Random.
How the Registry works
The Registry runs inside each Moleculer broker. On start‑up it registers the local service’s metadata (name, version, actions, events) and publishes that information over the transporter’s pub/sub channel. All nodes subscribed to the same channel receive the announcements and build a local view of the cluster. When a service invokes this.broker.call('other.action', args), the LoadBalancer consults the Registry view and picks a target node according to the selected strategy.
Key terms:
- Transporter – the messaging backbone (NATS, Redis, MQTT, etc.) that carries Registry events and user‑level messages.
- Registry – the internal component that syncs service metadata via the transporter.
- LoadBalancer – selects a node instance for each remote call.
NATS‑based worked example
We will run two services: an API that exposes a REST endpoint and a Worker that provides an add action. Both connect to a local NATS server.
1. Start NATS
# Run anywhere with Docker access
docker run -d --name moleculer-nats -p 4222:4222 nats:latest
This command pulls the NATS image and exposes port 4222. No special permissions are needed beyond Docker access.
2. Worker service (worker.js)
const { ServiceBroker } = require('moleculer');
const broker = new ServiceBroker({
transporter: 'nats://localhost:4222',
logger: console,
});
broker.createService({
name: 'worker',
version: 1,
actions: {
add(ctx) {
const { a, b } = ctx.params;
return a + b;
}
},
});
async function start() {
try {
await broker.start();
console.log('Worker service started');
} catch (err) {
console.error('Failed to start worker:', err);
process.exit(1);
}
}
start();
3. API service (api.js)
const { ServiceBroker } = require('moleculer');
const express = require('express');
const broker = new ServiceBroker({
transporter: 'nats://localhost:4222',
logger: console,
});
broker.createService({
name: 'api',
version: 1,
actions: {
async add(ctx) {
// Calls the worker.add action
return await broker.call('worker.add', ctx.params);
}
},
});
const app = express();
app.use(express.json());
app.post('/add', async (req, res) => {
try {
const result = await broker.call('api.add', req.params);
res.json({ result });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
async function start() {
try {
await broker.start();
const port = 3000;
app.listen(port, () => console.log(`API listening on :${port}`));
} catch (err) {
console.error('Failed to start API:', err);
process.exit(1);
}
}
start();
4. Run the services
# In separate terminals
node worker.js
node worker.js # start a second worker instance to see load balancing
node api.js
When each worker starts, you should see log lines similar to:
[Registry] Service discovered: worker@v1 from nodeID-abc123
The API logs will show calls being made to worker.add. With two workers running, observe that the nodeID in the call logs alternates, indicating the LoadBalancer is distributing traffic.
Limitations and practical checks
The Registry depends entirely on transporter connectivity. If the NATS broker becomes unreachable, nodes cannot exchange discovery messages, and the Registry may retain stale entries until the transporter’s heartbeat timeout expires (default ~30 seconds). During that window, calls could be routed to a disappeared node, resulting in timeout errors.
To verify failure detection:
- Stop one worker process (Ctrl+C).
- Watch the Registry logs for a line like
[Registry] Service lost: worker@v1 from nodeID‑def456after the heartbeat interval. - Submit more requests via the API; all should now be routed to the remaining worker.
Note: Moleculer does not perform application‑level health checks beyond the transporter’s heartbeat. If you need to exclude a node based on custom metrics (e.g., high latency), you must implement a custom LoadBalancer strategy or integrate an external service mesh.
Actionable closing
To adopt this pattern in your project:
- Choose a transporter that matches your infrastructure (NATS for low‑latency pub/sub, Redis if you already use it for caching).
- Set the same
transporteroption in every Broker instantiation. - Keep service
nameandversionidentical across instances of the same logical service. - Use the built‑in LoadBalancer or replace it with a custom strategy if you need advanced routing.
- Validate discovery by checking for “Service discovered” logs and observing load‑balancing behavior as described.
With these steps, Moleculer handles service location and traffic distribution automatically, letting you focus on business logic rather than manual endpoint management.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.