NEW live workshops: Leaving TIBCO or Solace for NATS Adding an enterprise backbone above MQTT Active/active multi-cloud architectures
All posts

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.

The short answer

If you are building a chat-like system with JetStream persistence:

  • One stream per space can reduce stream count and JetStream metadata overhead.
  • One stream per room can make room-level retention, deletion, and historical pagination simpler.
  • Consumers can matter as much as streams for scale planning. In clustered JetStream deployments, streams and consumers add coordination and state overhead.
  • Avoid creating a JetStream consumer per connected endpoint unless your expected endpoint count is comfortably within what you have tested.
  • Using Core NATS pub/sub for live delivery and JetStream for history is often a practical split.
  • Republish may help if you want a message accepted into JetStream and then forwarded for real-time delivery on another subject.

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.

Why this question comes up in chat systems

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:

  • spaces or teams
  • rooms or channels
  • users and devices
  • live subscriptions
  • historical reads
  • retention windows
  • deletes, edits, moderation events, and tombstones
  • per-room or per-user access patterns

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.

Option 1: one stream per space

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:

1
chat.<space_id>.<room_id>.messages
2
chat.<space_id>.<room_id>.events

The 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.

Advantages

This approach can be attractive because it keeps the number of streams lower.

Benefits include:

  • fewer streams to create and manage
  • less per-stream metadata and coordination overhead
  • easier handling when spaces are the natural operational boundary
  • simpler capacity planning if rooms are numerous or short-lived

If you expect many rooms per space, a stream-per-space model can avoid turning every room into its own JetStream asset.

Tradeoffs

The cost is that room-level operations are less isolated.

For example:

  • deleting one room’s history is a separate operation rather than a simple stream delete; a subject-filtered purge (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 time
  • per-room retention is less flexible: stream limits apply to the whole stream, and while max_msgs_per_subject can cap message count uniformly per subject, individual rooms in one shared stream cannot have different retention windows
  • room history is interleaved with other rooms, so paginating a single room relies on subject-filtered reads (a filtered consumer or the direct-get API) rather than a contiguous range of sequence numbers
  • retention, cleanup, or export workflows may need more application logic

This model often works best when spaces are long-lived, rooms are numerous, and room deletion is not a constant high-volume operation.

Option 2: one stream per room

The alternative is to create a stream for each room.

Conceptually:

1
chat.<space_id>.<room_id>.>

with each room’s subjects captured by that room’s stream.

Advantages

A stream-per-room model can make room-level lifecycle management straightforward.

Benefits include:

  • deleting a room’s history can be as simple as deleting the room’s stream
  • retention can be configured around the room as the unit of storage
  • history reads are naturally scoped to the room
  • sparse cross-room history is not a concern within an individual room stream

This can be a clean mental model, especially for systems where rooms are durable, important units with distinct retention requirements.

Tradeoffs

The cost is stream cardinality.

If every room becomes a stream, you need to consider:

  • total number of streams
  • rate at which streams are created and deleted
  • operational visibility and management of many assets
  • stream metadata and coordination overhead in a cluster
  • backup, restore, migration, and monitoring workflows

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.

Do not ignore consumer count

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:

  • 5,000 streams
  • 100,000 durable consumers

has a very different operational profile from a design with:

  • 5,000 streams
  • a small number of consumers used only for history reads or background processing

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.

A practical architecture split: Core NATS for live, JetStream for history

For chat applications, a useful pattern is:

  1. publish chat events into JetStream for persistence
  2. deliver live messages over Core NATS pub/sub
  3. query JetStream when clients need historical messages

This separates two different needs:

  • Live delivery: low-latency, best-effort fanout to currently connected clients
  • History: persisted retrieval for scrollback, reconnects, search pipelines, or moderation workflows

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.

Rough sizing guidance

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:

  • a few tens of thousands of streams and consumers combined can be a reasonable design range on commodity hardware
  • this is not a hard limit
  • creation rate matters; creating streams or consumers is not the same as publishing messages
  • creating hundreds per second may already be an important operational consideration
  • benchmark with your retention, replication, storage, subject, and consumer patterns

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.

What about multiple clusters or superclusters?

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:

  • where subjects are routed
  • where streams are placed
  • cross-cluster traffic patterns
  • operational ownership of tenants or spaces
  • stream and consumer creation rates
  • failover and disaster recovery expectations

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.

Choosing between the two models

A useful way to decide is to ask what your natural lifecycle boundary is.

Choose one stream per space when:

  • spaces are the primary tenant or operational unit
  • rooms are numerous or frequently created
  • you want to keep stream count lower
  • room deletion is relatively rare or can tolerate application-managed cleanup
  • shared retention across rooms is acceptable
  • consumers are not created per endpoint

Choose one stream per room when:

  • rooms have independent retention or deletion requirements
  • room history reads dominate and should be operationally simple
  • room count is bounded or comfortably within tested limits
  • stream creation rate is modest
  • deleting a room’s data cleanly is more important than minimizing stream count

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:

  1. Use Core NATS pub/sub for live room delivery.
  2. Use JetStream for persistent message history.
  3. Avoid a durable JetStream consumer per connected endpoint.
  4. Start with one stream per space if room count is high or unknown.
  5. Use subject structure that makes room history practical to retrieve.
  6. Move to one stream per room only when room-level deletion, retention, or read simplicity is worth the added stream cardinality.
  7. Load-test stream count, consumer count, stream creation rate, and history-read patterns before relying on estimates.

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.

Get the NATS Newsletter

News and content from across the community


Cancel