NATS Request-Reply: Synchronous Calls Without Manual Correlation
NATS request-reply routes answers back through temporary reply subjects, removing manual correlation IDs—if you handle timeouts and subscription limits.
13 May 2026, 03:08 UTC

The problem: waiting for an answer on a publish-subscribe bus
NATS is built around publish-subscribe: a publisher sends a message to a subject, and any subscriber receives it. There is no built-in request queue. If Service A needs a single answer from Service B before it can continue, the naive approach is to publish a request, generate a correlation ID, store it somewhere, and hope Service B publishes the answer back to a known subject where Service A can match it. That is fragile and duplicates work NATS already does.
The useful takeaway: NATS request-reply lets a client send a request with a reply subject attached. The server creates a short-lived subscription on that reply subject, routes the responder's answer back to the requester, and removes the subscription when the exchange ends. Your application does not need to invent correlation logic.
How the request-reply exchange works
In this pattern, "publish-subscribe" means messages are routed by subject name rather than addressed to a specific process. A "reply subject" is the destination where the responder sends its answer. A "temporary subscription" is an ephemeral server-side route that exists only until the reply arrives or the request times out.
The flow is:
- The requester publishes a message to a request subject and includes a unique reply subject in the message headers.
- The NATS server creates a temporary subscription on that reply subject so the response can find the requester.
- A responder subscribed to the request subject receives the message, reads the reply subject, and publishes its answer there.
- The requester receives the answer on its temporary subscription, which the server then tears down.
Worked example: requesting inventory availability
Imagine an order service that must check whether a SKU is available before placing an order. The following Go pseudocode shows the requester side. Replace inventory.check and the timeout with values for your environment.
package main
import (
"fmt"
"log"
"time"
"github.com/nats-io/nats.go"
)
func main() {
nc, err := nats.Connect("nats://localhost:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
request := []byte(`{"sku":"ABC-123","quantity":1}`)
// Request blocks until a responder answers or the timeout expires.
msg, err := nc.Request("inventory.check", request, 2*time.Second)
if err != nil {
log.Fatalf("request failed: %v", err)
}
fmt.Printf("response: %s\n", string(msg.Data))
}
The responder looks like this:
nc.Subscribe("inventory.check", func(msg *nats.Msg) {
if msg.Reply == "" {
return
}
// msg.Reply is the temporary reply subject created by NATS.
nc.Publish(msg.Reply, []byte(`{"available":true}`))
})
Where to run: inside any service that has the NATS client library installed and network access to the NATS server. Required permissions: the NATS user must be allowed to publish to inventory.check and to publish/subscribe on reply subjects. Expected check: the requester logs the responder's JSON payload within two seconds. Risk: if no responder is subscribed to inventory.check, nc.Request returns a timeout error after two seconds instead of blocking forever.
Note: exact client API signatures vary by language and library version. Verify the Request method and timeout behavior in your client's documentation before using it in production.
Trade-offs and limits
Request-reply is not free. Each in-flight request holds a temporary subscription on the server. Under heavy load, hundreds of concurrent requests can push subscription or connection counts toward server limits, increasing memory use and risking connection exhaustion. If a requester crashes before receiving an answer, the subscription can linger until the server cleans it up.
The pattern also adds round-trip latency compared with fire-and-forget publishing, and NATS does not retry a request automatically. Your client code must own the timeout and decide whether to retry, fail fast, or degrade gracefully.
Verify the behavior before you depend on it
Run a small load check in a staging environment. From a terminal with access to the NATS monitoring endpoint, inspect connection and subscription counts:
# Run from a host that can reach the NATS server monitoring port.
# Replace localhost:8222 with your server address. Requires read access to monitoring.
nats server report --addr localhost:8222
Expected check: while 50 concurrent requests are in flight, the active subscription count should rise, then return to baseline once responses arrive. If subscriptions stay elevated, your clients are not closing connections or the server is not cleaning up temporary routes quickly enough.
Practical way to check the result: open one terminal running a script that sends 50 rapid requests with a long timeout, and a second terminal watching nats server report or the server's metrics endpoint. Confirm the subscription count spikes and then falls back to the pre-test baseline within seconds of the responses completing.
Closing
Use NATS request-reply when you need one answer from one responder and can tolerate a round trip. Set an explicit timeout on every request, monitor temporary subscription counts under load, and avoid the pattern for high-volume fire-and-forget events. Verify your client library's request helper and your server's monitoring flags before rolling out.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.