An inactive stream is an unsealed JetStream stream that has received no new messages across the observed time range — its last sequence number hasn’t changed. The stream exists, consumes storage and Raft resources, but no publisher is writing to it. Sealed streams are excluded from this check because they’re intentionally frozen.
Every stream in a NATS cluster has an ongoing cost. File-backed streams occupy disk space. Memory-backed streams consume RAM. Replicated streams (R3, R5) multiply these costs across servers and add Raft group overhead — each replica participates in heartbeats, elections, and log compaction even when no messages are flowing. A cluster with dozens of inactive streams is paying the infrastructure and operational cost of data it no longer uses.
The Raft overhead is particularly wasteful for inactive replicated streams. Each Raft group runs periodic heartbeats between the leader and followers. With enough inactive R3 streams, the cumulative heartbeat traffic and CPU cost becomes non-trivial — not because any single group is expensive, but because the aggregate adds up. The meta cluster tracks every stream, so more streams mean a larger meta state, slower snapshots (see META_004), and more work during leader elections.
Inactive streams also create operational clutter. When operators list streams to debug an issue, they wade through inactive entries alongside active ones. Stream reports mix idle and busy streams, making it harder to spot real problems. Over time, the team loses track of which streams are intentionally inactive, which are leftover from decommissioned services, and which are simply forgotten. The uncertainty alone creates friction in operational workflows.
Decommissioned services that didn’t clean up their streams. A service was retired or replaced, but nobody deleted its streams. The streams persist with their last message intact, accumulating Raft and storage costs indefinitely.
Test or development streams left in production. During development, migration testing, or load testing, streams were created in the production cluster and never cleaned up. They may contain test data that’s no longer relevant.
Seasonal or batch workloads. Some streams receive data only during specific business cycles — monthly reporting, quarterly batch imports, annual events. Between cycles, the stream is legitimately inactive. These streams are expected and should be excluded from cleanup.
Renamed or moved subjects. The publishing service changed its subject namespace (e.g., from orders.v1.> to orders.v2.>), and a new stream was created for the new subjects. The old stream still exists, bound to subjects that nobody publishes to anymore.
Failed provisioning or migration. A stream was created as part of an automated provisioning pipeline, but the pipeline failed before deploying the corresponding publishing service. The stream exists but was never activated.
nats stream reportLook at the Messages column and Last Sequence. Streams that haven’t changed across your observation period are candidates. For more detail:
nats stream listnats stream info <stream_name>The State section shows Last Sequence and the timestamp of the last message. If the last message timestamp is days, weeks, or months old, the stream is inactive.
# List streams with their last message timenats stream list --json | jq '.[] | {name: .config.name, last_ts: .state.last_ts, messages: .state.messages, bytes: .state.bytes}'Sort by last_ts to find the stalest streams. Cross-reference with team ownership records to determine whether inactivity is expected.
nats consumer list <stream_name>An inactive stream with active consumers pulling from it is a data pipeline that’s caught up — the publishers stopped but consumers are still draining. An inactive stream with zero consumers or only inactive consumers (see OPT_IDLE_003) is likely abandoned.
# Check if anything is publishing to the stream's subjectsnats sub "<stream_subject>" --count 1 --timeout 30sIf no messages arrive within a reasonable timeout on the stream’s subjects, no publisher is active.
Before deleting anything, categorize each inactive stream:
# Check stream configuration for clues about ownershipnats stream info <stream_name> --json | jq '.config'Stream names, subject patterns, and description fields often reveal the owning service or team.
For streams you’ve confirmed are abandoned, back up the data before deletion:
# Back up stream data to a local directorynats stream backup <stream_name> /tmp/stream-backups/<stream_name>
# Delete the streamnats stream delete <stream_name> --forceFor streams you want to preserve but prevent accidental writes to:
# Seal the stream — makes it read-only, prevents new messagesnats stream seal <stream_name>Sealed streams are excluded from this check and clearly signal intentional inactivity.
If an inactive stream must remain but doesn’t need fault tolerance while idle, reduce its replica count:
1// Go — reduce replicas on an inactive stream2js, _ := nc.JetStream()3
4_, err := js.UpdateStream(&nats.StreamConfig{5 Name: "LEGACY_ORDERS",6 Subjects: []string{"legacy.orders.>"},7 Replicas: 1, // reduce from R3 to R1 while inactive8})1// TypeScript (nats.js) — reduce replica count2const jsm = await nc.jetstreamManager();3
4await jsm.streams.update("LEGACY_ORDERS", {5 subjects: ["legacy.orders.>"],6 num_replicas: 1,7});This eliminates the Raft overhead and 2/3 of the storage cost. Scale back to R3 when the stream becomes active again.
Build stream hygiene into your operational practice:
Require retention limits on all streams. Every stream should have max_age, max_bytes, or max_msgs set (see OPT_SYS_001). This prevents abandoned streams from holding data indefinitely.
Tag streams with owner metadata. Use stream descriptions or a naming convention that encodes the owning team or service:
nats stream add TEAM_PAYMENTS_TRANSACTIONS \ --subjects "payments.transactions.>" \ --description "Owner: payments-team, Slack: #payments-eng" \ --max-age 30d \ --replicas 3# Find streams with no messages in the last 30 daysnats stream list --json | jq '[.[] | select(.state.last_ts < (now - 2592000 | todate))] | .[].config.name'A sealed stream is intentionally frozen — an operator explicitly sealed it to prevent new writes, typically to preserve a complete dataset as an archive. An inactive stream is simply one that nobody is publishing to, which may be intentional or may be an oversight. Sealed streams are excluded from this check because their inactivity is deliberate.
Yes. Deleting a stream immediately destroys all its consumers. Any applications subscribed via those consumers will receive errors. Verify that no active consumers exist before deletion — check with nats consumer list <stream_name> and confirm each consumer is also inactive (OPT_IDLE_003) or abandoned.
There’s no universal answer — it depends on your workload patterns. A stream for daily batch processing that’s been inactive for 2 days is normal. A stream for a decommissioned service that’s been inactive for 3 months is a cleanup candidate. The check flags streams inactive across the entire observed time range; use your knowledge of the workload’s expected publish frequency to decide.
No. Once a stream is deleted, its data is permanently removed from all servers. This is why nats stream backup before deletion is critical. The backup can be restored with nats stream restore if you need the data later. For streams you’re unsure about, sealing (making read-only) is a safer intermediate step than deletion.
Individually, the impact is small. A single idle R1 stream costs minimal disk and no CPU beyond metadata. But costs compound: 50 inactive R3 streams means 150 Raft groups running heartbeats, 150 replicas consuming disk, and 50 extra entries in the meta cluster state. At that scale, the aggregate overhead is measurable in CPU, network, and snapshot times — and it’s entirely waste.
With 100+ always-on audit Checks from the NATS experts, Insights helps you find and fix problems before they become costly incidents.
No alert rules to write. No dashboards to maintain.
News and content from across the community