A community member asked a common JetStream design question for chat applications: should messages be stored in one larger stream per workspace or “space,” with room IDs encoded in subjects, or in many smaller streams, such as one stream per room?
There is no universal answer. It is mostly a sizing and operations tradeoff: how many rooms you expect, how many consumers you create, how long you retain history, how often rooms are deleted, and whether JetStream is serving real-time delivery, historical reads, or both.
If you are building a chat-like system with JetStream persistence:
As a rough operational guide, a few tens of thousands of streams and consumers combined may be reasonable on commodity hardware, but that is not a hard product limit or a guarantee for every workload. Treat it as a planning signal and benchmark your own pattern.
Chat systems are deceptively simple examples for messaging infrastructure. A small system may have a few spaces, a few rooms, and a modest number of connected users. A larger one can quickly multiply across dimensions:
JetStream can persist messages, but it is not magic storage that scales infinitely in every dimension at once. The design should make the common operations cheap and the uncommon operations acceptable.
A common approach is to create one stream per higher-level grouping, such as a workspace, tenant, guild, or space. Subjects then partition messages by room.
For example, conceptually:
1chat.<space_id>.<room_id>.messages2chat.<space_id>.<room_id>.eventsThe exact subject taxonomy is application-specific, but the idea is that all rooms in a space land in the same stream while subjects preserve enough structure to fetch or filter room-specific history.
This approach can be attractive because it keeps the number of streams lower.
Benefits include:
If you expect many rooms per space, a stream-per-space model can avoid turning every room into its own JetStream asset.
The cost is that room-level operations are less isolated.
For example:
nats stream purge <stream> --subject "chat.<space_id>.<room_id>.>", backed by the JetStream stream purge API with a filter) can remove all of a room’s messages in a single call, so this does not require deleting messages one at a timemax_msgs_per_subject can cap message count uniformly per subject, individual rooms in one shared stream cannot have different retention windowsThis model often works best when spaces are long-lived, rooms are numerous, and room deletion is not a constant high-volume operation.
The alternative is to create a stream for each room.
Conceptually:
1chat.<space_id>.<room_id>.>with each room’s subjects captured by that room’s stream.
A stream-per-room model can make room-level lifecycle management straightforward.
Benefits include:
This can be a clean mental model, especially for systems where rooms are durable, important units with distinct retention requirements.
The cost is stream cardinality.
If every room becomes a stream, you need to consider:
A few thousand room streams may be entirely manageable for a given environment. A few tens of thousands may still be plausible if the rest of the design is conservative and tested. But an unbounded stream-per-room design can become a scaling problem before the message volume itself does.
When sizing a JetStream design, it is easy to focus only on streams. Consumers can be just as important.
In clustered JetStream deployments, streams and consumers maintain state and coordination. This overhead is largest for replicated assets: a replicated stream or consumer (for example, R3) is backed by its own Raft group, while R1 assets are cheaper but still carry metadata, memory, and storage overhead. Durable consumers in particular are not free. If every connected user, browser tab, mobile device, or endpoint gets its own JetStream consumer, consumer count may become the limiting factor before stream count does.
For example, a design with:
has a very different operational profile from a design with:
If your real-time delivery path is Core NATS pub/sub and JetStream is used mainly for persistence and historical retrieval, you avoid creating one persistent JetStream consumer per live endpoint. That can change the stream-per-space vs stream-per-room decision substantially.
For chat applications, a useful pattern is:
This separates two different needs:
If clients do not require JetStream’s consumer state for live delivery, you can reduce the number of JetStream consumers significantly.
JetStream’s republish feature may be useful in this kind of design. When a stream is configured for republish, each message stored in the stream is also re-emitted to a configured destination subject as an ordinary Core NATS message — best-effort, with no per-subscriber state — and the re-emitted message carries headers that identify the originating stream and sequence. Whether that is the right fit depends on your subject design and delivery semantics, but it is worth considering when you want persistence and live fanout without making every client a durable JetStream consumer.
There is no single number that applies to every cluster, machine type, storage device, replication factor, workload, or operational tolerance.
That said, a conservative planning heuristic is:
The important point is that stream and consumer cardinality are first-class dimensions of your design. They should be load-tested, monitored, and bounded like message rate and storage volume.
Horizontal scaling is possible by distributing work across multiple NATS clusters, including supercluster topologies. This can help when tenants, spaces, regions, or other boundaries can be placed on different clusters.
However, adding clusters does not remove the need for careful design. You still need to consider:
In particular, stream and consumer creation is coordinated through a JetStream metadata leader, and in a supercluster that creation-rate ceiling is effectively shared across the whole supercluster rather than reset per cluster. JetStream domains can partition a deployment into independent JetStream systems, each with its own metadata, which is one way to isolate that boundary. Validate the behavior that matters to your deployment rather than assuming that adding clusters linearly removes every bottleneck.
A useful way to decide is to ask what your natural lifecycle boundary is.
Choose one stream per space when:
Choose one stream per room when:
A hybrid is also possible. For example, you might use one stream per space for small or low-traffic rooms, but isolate high-volume or special-retention rooms into their own streams. Do not add that complexity unless it solves a concrete operational problem.
For many chat applications, a reasonable starting point is:
The best design is not the one with the fewest streams or the most isolated streams. It is the one where your common operations remain simple, your cardinality is bounded, and your operational limits are understood.
Want help from the NATS experts? Meet with our architects to get help tailored to your use case and environment.



News and content from across the community