Skip to main content

Shaping the stream

The ORDERS stream you created back on the Your first stream page has no limits. It keeps every message forever, on however much disk the server has. That was fine for learning, but not for production.

Without a limit, the stream keeps growing until it fills the disk and takes the server down with it.

This page covers two settings. The limit is the cap that decides when the stream is full. The Discard policy decides which message the server discards when the stream hits that limit: the new one or the oldest stored one.

The limit

A stream under the default Limits retention policy keeps messages until a limit forces it to discard them. You saw that policy in the config printout on the Your first stream page. A limit is a ceiling on the stream. You set it with one of three options, depending on how you want to measure the stream:

  • MaxAge caps how old a message may get. Set it to seven days and a message is discarded roughly seven days after it was stored. Most order systems use this one, since you rarely need an order event from last quarter in a live stream.
  • MaxBytes caps how much disk the stream may use. Set it to one gigabyte and the stream never grows past a gigabyte, no matter how old or new the messages are. This option protects the server itself.
  • MaxMsgs caps how many messages the stream may hold. Set it to one million and the millionth-and-first message forces a discard. Use this when message count, rather than size or age, is what you reason about.

The three options work separately, and all of them are active at once. Whichever one is reached first triggers a discard. You don't have to set all three. Set the ones that match how you think about the stream, and leave the rest unlimited.

MaxAge evicts by the clock; MaxMsgs evicts by the count. Same discard, two different triggers:

Cap the ORDERS stream

Give ORDERS a seven-day age limit and a one-gigabyte ceiling. The nats stream edit command changes an existing stream in place, so the three messages already stored stay where they are.

#!/bin/bash
# Cap the ORDERS stream so it cannot grow without bound.
#
# `nats stream edit` changes an existing stream in place. The three
# messages already stored stay where they are; only the configuration
# changes. The command prints a diff of what will change and asks for
# confirmation before applying it.
#
# --max-age sets MaxAge: the oldest a message may get. 7d means a
# message is removed roughly seven days after it was stored.
# --max-bytes sets MaxBytes: the most disk the stream may occupy on
# disk. 1GiB caps it at one gibibyte (about a gigabyte); the stream
# never grows past it. nats reports the cap back as "1.0 GiB".
#
# MaxMsgs is left unset, so message count stays unlimited. Discard
# policy is left at its default (Old), so when a limit is hit the
# server drops the oldest messages to make room.

nats stream edit ORDERS \
--max-age=7d \
--max-bytes=1GiB

nats stream edit shows the change and asks for confirmation before applying it. Confirm, then read the stream back:

nats stream info ORDERS

The configuration block now reports the two limits instead of unlimited:

Configuration:

Subjects: orders.>
Retention Policy: Limits
Discard Policy: Old
Maximum Messages: unlimited
Maximum Bytes: 1.0 GiB
Maximum Age: 7d0h0m0s
Maximum Message Size: unlimited

Maximum Messages is still unlimited, because you set only age and bytes. (Maximum Message Size, also in the block, is a different limit — a cap on a single message rather than the whole stream — and stays unlimited here.) The stream now has clear bounds: it can't grow past a gigabyte, and it can't hold anything older than a week.

The Discard policy

The Discard policy controls what happens at the moment a new message would push the stream past a limit: the server discards the new message or the oldest stored one. It has two settings.

Discard Old is the default, and it's what you have right now. When a limit is hit, the server discards the oldest messages to make room for the new one. The publish always succeeds: the newest message is stored, the oldest is discarded.

Discard New is the opposite. When a limit is hit, the server discards the new message: it rejects the publish, which fails with an error. Messages already stored are never discarded. Once the stream is full, it refuses more.

For ORDERS, Discard Old is the right choice. A live order stream wants the most recent week of events. If disk pressure forces a trade-off, the order from eight days ago is the one to discard, not today's. Leave the default in place.

Use Discard New when discarding an old message would lose data you're required to keep, and you'd rather slow the publisher down than lose history. The publisher then has to handle the rejected publish, which is why it's the less common choice.

Limits apply to the stream, not the consumer

A limit discards a message for everyone. When MaxAge discards a message, it's gone from the stream, and every consumer reading that stream loses access to it at once.

Limits and consumers are separate decisions. The shipping consumer's position and the analytics consumer's filter don't protect a message from the stream's limits. If a consumer is too slow and a message ages out before that consumer reads it, the message is gone. The next page returns to that risk, where the retention policy itself changes.

Per-subject limits

A stream can also limit messages per subject rather than across the whole stream, which is useful when orders.> should keep, say, the last hundred messages for each individual subject. That's the MaxMsgsPerSubject option.

The full set of stream limit options is documented in Reference → Stream Configuration. We use only MaxAge, MaxBytes, and Discard here.

Pitfalls

Limits are easy to set and easy to misread. Two traps account for most of the surprises.

Discard Old discards the oldest message silently. Discard Old never fails a publish. When a limit is hit, the server discards the oldest message and the publish succeeds. That's what you want for a rolling window. But you lose data without notice if you expected the stream to refuse the new message. The publisher gets no warning, and the order from eight days ago is gone. When you must keep history and would rather slow the publisher down, switch to Discard New and handle the rejected publish. It fails with maximum bytes exceeded (or maximum messages exceeded) instead of discarding anything:

#!/bin/bash
# Switch ORDERS from Discard Old to Discard New so a full stream pushes
# backpressure to the publisher instead of silently dropping old orders.
#
# Under Discard Old (the default) a publish that exceeds a limit always
# succeeds, because the server drops the oldest message to make room and
# tells the publisher nothing. If you need to keep history, that silent
# drop is data loss you never see.
#
# --discard new flips the trade: when a limit is hit, the server rejects
# the new message and the publish fails with "maximum bytes exceeded" (or
# "maximum messages exceeded"). The publisher now feels the limit.

# Force the full condition so you can see the rejection. Discard New never
# drops messages that are already stored, so switching ORDERS to it and
# capping it at one message leaves the orders from earlier pages in place and
# puts the stream instantly over its limit -- both in one edit.
nats stream edit ORDERS --discard new --max-msgs 1

# ORDERS is now full. Under Discard New this publish fails instead of
# succeeding silently, so the publisher can retry, alert, or shed load:
nats pub orders.created \
'{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}'
# -> nats: maximum messages exceeded

# Put ORDERS back the way it was: Discard Old and no message-count cap. The
# 7-day age and 1 GiB byte limits set earlier stay in place.
nats stream edit ORDERS --discard old --max-msgs -1

The same quiet discard applies to MaxAge and MaxBytes together. The two limits work separately, so whichever is reached first triggers the discard. A seven-day MaxAge does not guarantee seven days of history. If traffic spikes, MaxBytes can be reached first and discard messages that are only hours old. Set MaxBytes for your busiest traffic, not your average, if the age window matters to you.

Whole-stream limits don't balance across subjects. MaxMsgs, MaxBytes, and MaxAge measure ORDERS as a whole, across every subject under orders.>. A high volume of orders.created counts toward the same ceiling as orders.shipped, so Discard Old can discard a shipped order to make room for a created one. When each subject needs its own limit, add a per-subject ceiling with MaxMsgsPerSubject:

#!/bin/bash
# Add a per-subject ceiling to ORDERS so one noisy subject cannot evict
# another subject's messages.
#
# MaxMsgs, MaxBytes, and MaxAge all measure the stream as a whole,
# across every subject under orders.>. If orders.created floods in, its
# messages count toward the same ceiling as orders.shipped, and Discard
# Old can drop a shipped order to make room for a created one.
#
# --max-msgs-per-subject sets MaxMsgsPerSubject: a separate ceiling
# applied to each individual subject. Set it to 100000 and every subject
# keeps its own most-recent 100000 messages, independent of how loud its
# neighbors are.

nats stream edit ORDERS --max-msgs-per-subject=100000

# Read the stream back; the per-subject limit now appears alongside the
# whole-stream limits.
nats stream info ORDERS

Under Discard Old, a per-subject ceiling discards the oldest message for that subject once it fills. Discard New doesn't change that on its own: by default the per-subject limit still rolls, discarding the subject's oldest message rather than rejecting the publish. Making a full subject reject takes a second setting, DiscardNewPerSubject (the discard_new_per_subject config field), on top of Discard New. With both, a publish past the per-subject ceiling fails with maximum messages per subject exceeded, a third rejection string alongside the whole-stream maximum bytes exceeded and maximum messages exceeded. The field is in Reference → Stream Configuration.

Where you are

You now have:

  • an ORDERS stream capped at a seven-day MaxAge and a 1 GiB MaxBytes ceiling
  • the three messages from earlier still stored (editing limits doesn't discard messages that already sit within them)
  • Discard Old in place, so a future limit discards the oldest order, never the newest

What's next

You set limits under the default Limits retention policy. The next page covers the policy choice itself: the three retention policies, Limits versus Interest versus WorkQueue, and which one to use when.

See also