Skip to main content

Pull consumers in depth

The shipping consumer delivers the next message to a worker when the worker asks. That ask is a pull. The page that created the consumer treated a pull as a single "give me one message."

A worker often wants more than one message at a time. It might want a handful, process them, and come back. It might want a continuous flow where new messages arrive as soon as they reach the stream. This page covers both patterns and the fields that bound them.

The shipping consumer doesn't change. It stays a pull consumer with explicit ack. What changes is how your code drives it.

Two ways to pull

There are two pull patterns, and every client library names them the same way.

Fetch asks for a batch of up to N messages. The call returns when the batch is full or when a timeout expires, whichever comes first. You get a finite set of messages, you process them, and the call is done. To keep going, you fetch again.

Consume sets up a continuous flow. You pass it a function. The library sends pull requests in the background and adds new ones as messages arrive. It calls your function for each message. It runs until you stop it instead of returning after a batch.

Use fetch when your code wants control over each round, such as a cron job that drains what's queued or a request handler that takes a few messages. Use consume when you want a long-running worker that processes messages as fast as they arrive. Most services use consume.

Fetch a batch

A fetch names a batch size and a timeout. Here's a worker asking for up to ten messages and waiting up to two seconds for them:

#!/bin/bash
# Fetch a batch of up to 10 messages from the shipping pull consumer,
# waiting up to 2 seconds for them. nats consumer next is a single
# fetch; --count is the batch size, --wait is the timeout.
nats consumer next ORDERS shipping --count 10 --wait 2s

Two outcomes are normal.

If ten messages are queued, the call returns all ten immediately. The worker processes and acks them, then fetches again.

If only three messages are queued, the call returns those three and then waits up to two seconds for a fourth. When the two seconds pass, it returns the three it has. The timeout is the ceiling, so a fetch always returns within it.

From the CLI, nats consumer next is a single fetch. The --count flag is the batch size:

#!/bin/bash
# A single fetch from the CLI. --count sets the batch size: ask for up
# to 5 messages. Run this again to walk the stream a batch at a time.
nats consumer next ORDERS shipping --count 5

Run it twice and you walk the stream a batch at a time. The consumer's cursor advances as messages are acked, the same way it did one message at a time on the consumer page.

Consume a continuous flow

A long-running worker shouldn't loop on fetch by hand. The consume pattern does the looping for you. It keeps pull requests open so a new message is delivered as soon as it's stored in the stream:

#!/bin/bash
# Consume a continuous flow from the CLI. A large --count keeps a long
# pull open so new messages are delivered the moment they land in the
# stream. --ack acknowledges each message as it is received. Ctrl-C to
# stop.
nats consumer next ORDERS shipping --count 1000 --ack

Your function runs once per message and acks on success. The library handles the pull requests, sends new ones as the old ones empty, and keeps going until you stop it. Most order-processing workers use this pattern.

The two fields that bound a pull

Both patterns issue the same underlying pull request, and two fields on that request decide how much a single pull returns:

  • batch: the maximum number of messages this pull may return. A bigger batch means fewer round trips and higher throughput. A smaller batch means lower latency per message and less work lost if the worker dies mid-batch.
  • expires: how long the server holds the pull open waiting for messages before it returns what it has. This is the timeout from the fetch above. It bounds latency on a quiet stream.

Client libraries set defaults for both, and the consume pattern keeps a batch and an expiry in flight for you, so a plain consume loop behaves well without tuning.

For the full set of pull request fields, see Reference → Consumer API. This page uses only batch and expires.

Pitfalls

A few defaults cause problems once shipping carries real order traffic. Know these before you tune anything.

An empty fetch is normal. When no orders are queued, a fetch returns nothing once expires elapses. The server replies with a 404 No Messages or 408 Request Timeout status, and every client reports that as an empty batch (the CLI exits non-zero). A worker that treats an empty fetch as a failure fails on a quiet stream. An empty result means nothing is available right now, so keep looping: wait and fetch again.

#!/bin/bash
# A fetch on a drained consumer returns nothing. nats consumer next exits
# non-zero when the pull times out with no messages waiting. Treat that as
# "nothing right now," not a failure: sleep and fetch again.
if nats consumer next ORDERS shipping --count 10 --timeout 2s; then
echo "processed a batch"
else
echo "no orders waiting, will retry"
sleep 1
fi

A fetch with no expiry can stall. Drop expires and a fetch has no time limit. A fetch waiting for a batch that never fills hangs until enough orders arrive. Always set an expiry so a quiet stream returns control to your loop instead of blocking it. The CLI sets one for you from --timeout; in code, pass expires explicitly.

MaxAckPending set too low limits throughput. This is the limit on un-acked messages the consumer hands out before it waits for acks. If you set it well below your batch size (a limit of ten against a batch of 100), the server delivers ten orders, then stops until your worker acks, no matter how large a batch you ask for. Keep it at or above your batch size. The default is 1000; lower it only when you know the in-flight count you want. The worker pool shares this single limit across every worker, so it matters even more there: see the worker pool page.

A batch set too large uses more memory than expected. batch counts messages, not bytes, so a large batch against large orders can pull more into memory in one round than you expect. The Reference link below covers a companion field, max_bytes, that caps the total size a pull may return; whichever limit is hit first ends the pull.

Where you are

You still have one stream, ORDERS, and one pull consumer, shipping. What changed is how you drive it. You can fetch a bounded batch when your code wants each round, or consume a continuous flow when you want a long-running worker. In both cases you bound a single pull with batch and expires.

What's next

The next page puts several workers on the shipping consumer at once and shows the server splitting the stream between them: a pool of workers sharing one cursor. That's also where MaxAckPending, the limit on un-acked messages across the whole consumer, starts to matter, since the pool shares one limit between every worker.

See also