This episode covers the request-reply pattern for RPC between microservices with nats request and timeouts, then queue groups for load balancing between consumers and their comparison with competing consumers.

Episode 4 strengthened your routing. Episode 5 arms you with two patterns that make NATS the backbone of microservices: request-reply for synchronous RPC-style communication, and queue groups for dividing workloads horizontally.
Both use a mechanism you already know — subjects — but with different semantics. Let's break them down one by one.
The NATS request-reply pattern works in three steps: the caller publishes a request to a subject with an automatic reply subject, the NATS server inserts that reply subject into the message header, then the handler publishes its answer to that same reply subject.
client --> request to subject:auth.login [reply: _INBOX.x]
auth service catches the subject
service publishes answer to _INBOX.x
client receives the answerThe subscriber handler processing the request doesn't need to know the caller's identity — it just uses msg.respond() to reply.
The fastest way to see this pattern come alive is through the CLI:
nats reply 'auth.login' "OK: token-abc123"
nats request 'auth.login' '{"username":"arman"}'nats reply 'auth.login' "OK: token-abc123" sets up a handler that answers every request, and nats request 'auth.login' '{...}' sends a request then waits for the answer. The CLI output shows OK: token-abc123.
Request-reply is a synchronous operation — the caller waits. The wait must be bounded so it doesn't hang:
nats request auth.login '{"username":"arman"}' --timeout=2sThe --timeout=2s flag above makes the CLI stop waiting after 2 seconds. If the handler is slow, the caller gets a timeout error instead of hanging forever. This habit must be mirrored in every client library.
With nats-py, request-reply is as easy as calling a function:
import asyncio
import nats
async def main():
nc = await nats.connect("nats://localhost:4222")
async def handler(msg):
await msg.respond(b"pong")
await nc.subscribe("ping", cb=handler)
reply = await nc.request("ping", b"ping-request", timeout=2)
print(reply.data)
await nc.close()
asyncio.run(main())await nc.request("ping", b"ping-request", timeout=2) sends the request and waits up to 2 seconds for an answer. On the other side, await msg.respond(b"pong") answers the request — these two lines are the heart of the NATS RPC pattern.
A practical rule for timeouts in distributed systems: set a timeout at the caller, and set it smaller than the downstream handler's timeout. If service A calls B and B calls C, A's timeout toward B must be smaller than B's timeout toward C — so errors don't pile up in layers.
Tip
Use request-reply for operations that need an immediate answer: verification, authorization, data retrieval. For workloads that can be processed asynchronously, just publish without waiting for a reply — it's faster and doesn't block.
A queue group turns publish/subscribe into competing consumers: a group of subscribers sharing a queue name, where each message is received by only one member. The NATS server round-robins between members.
nats sub 'jobs.process' --queue workers
nats sub 'jobs.process' --queue workersBoth nats sub 'jobs.process' --queue workers commands form a single workers queue group — two worker processes share the messages in turn. With 10 messages, each receives about 5.
A queue group is a very simple scaling mechanism: to handle more load, just run additional consumer instances with the same queue name. No server configuration changes, no re-deploys.
publisher --> jobs.process --> worker-1 (queue: workers)
--> worker-2 (queue: workers)
--> worker-3 (queue: workers)The diagram above shows three workers sharing the jobs.process subject — each message lands on exactly one worker. This differs from regular publish/subscribe where all subscribers receive a copy.
| Characteristic | Publish/Subscribe | Queue Group |
|---|---|---|
| Message recipients | All subscribers | One member per message |
| Purpose | Broadcast events | Load balancing tasks |
| Example | orders.created | jobs.process |
| Naming | No queue | --queue workers |
nats sub subject --queue name turns a regular subscriber into a queue group member. The choice between broadcast and load balancing shapes your system's structure.
In client libraries, the queue group is specified at subscribe time:
import { connect, StringCodec } from "nats";
const nc = await connect({ servers: "nats://localhost:4222" });
const sc = StringCodec();
nc.subscribe("jobs.process", { queue: "workers" }, (err, msg) => {
console.log("diproses oleh worker:", sc.decode(msg.data));
});
nc.publish("jobs.process", sc.encode("job-1"));nc.subscribe("jobs.process", { queue: "workers" }, handler) registers the subscriber as a member of the workers queue. Run two instances of this file, publish a few jobs, and watch the round-robin distribution.
Info
Queue groups balance load but don't guarantee ordering. For workloads that need sequential processing or delivery guarantees, combine them with JetStream — precisely the work queue pattern in episode 11.
Episode 5 equipped you with two productive communication patterns: request-reply for synchronous RPC with nats request and disciplined timeouts, and queue groups for horizontal load balancing between consumers with the --queue flag.
Key takeaways:
nats request and nats reply make it easy to test RPC patterns from the CLI.In episode 6 next, we'll discuss CLI & client libraries — the range of nats pub, nats sub, nats req, nats stream, nats consumer, and nats account commands, then write complete programs in Go, Python, and Node.js to connect, publish, subscribe, and request. This is where your daily toolkit starts to take shape.