Checks/OPT_IDLE_002

NATS Inactive Stream: What It Means and How to Fix It

Severity
Info
Category
Health
Applies to
Idle Resources
Check ID
OPT_IDLE_002
Detection threshold
last_seq unchanged across time range (excludes sealed streams)

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.

Why this matters

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.

Common causes

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

How to diagnose

List all streams and identify inactive ones

Terminal window
nats stream report

Look at the Messages column and Last Sequence. Streams that haven’t changed across your observation period are candidates. For more detail:

Terminal window
nats stream list

Check the last message timestamp for a specific stream

Terminal window
nats 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.

Identify all streams with stale last sequences

Terminal window
# List streams with their last message time
nats 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.

Check if the stream has any active consumers

Terminal window
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.

Verify the stream’s subjects still have publishers

Terminal window
# Check if anything is publishing to the stream's subjects
nats sub "<stream_subject>" --count 1 --timeout 30s

If no messages arrive within a reasonable timeout on the stream’s subjects, no publisher is active.

How to fix it

Immediate: assess and categorize inactive streams

Before deleting anything, categorize each inactive stream:

  1. Abandoned — service decommissioned, no owner, no consumers. Safe to delete.
  2. Seasonal — legitimate workload that will resume. Leave it alone (or seal it until next cycle).
  3. Unknown — can’t determine ownership or purpose. Tag for investigation.
Terminal window
# Check stream configuration for clues about ownership
nats stream info <stream_name> --json | jq '.config'

Stream names, subject patterns, and description fields often reveal the owning service or team.

Short-term: archive and delete abandoned streams

For streams you’ve confirmed are abandoned, back up the data before deletion:

Terminal window
# Back up stream data to a local directory
nats stream backup <stream_name> /tmp/stream-backups/<stream_name>
# Delete the stream
nats stream delete <stream_name> --force

For streams you want to preserve but prevent accidental writes to:

Terminal window
# Seal the stream — makes it read-only, prevents new messages
nats stream seal <stream_name>

Sealed streams are excluded from this check and clearly signal intentional inactivity.

Short-term: reduce cost of inactive streams you keep

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 stream
2
js, _ := 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 inactive
8
})
1
// TypeScript (nats.js) — reduce replica count
2
const jsm = await nc.jetstreamManager();
3
4
await 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.

Long-term: implement stream lifecycle management

Build stream hygiene into your operational practice:

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

  2. Tag streams with owner metadata. Use stream descriptions or a naming convention that encodes the owning team or service:

Terminal window
nats stream add TEAM_PAYMENTS_TRANSACTIONS \
--subjects "payments.transactions.>" \
--description "Owner: payments-team, Slack: #payments-eng" \
--max-age 30d \
--replicas 3
  1. Schedule periodic audits. Monthly or quarterly, review all streams and flag those inactive for longer than one business cycle:
Terminal window
# Find streams with no messages in the last 30 days
nats stream list --json | jq '[.[] | select(.state.last_ts < (now - 2592000 | todate))] | .[].config.name'
  1. Automate alerts for stale streams. Synadia Insights evaluates this check automatically across your entire deployment, flagging inactive streams before they accumulate significant cost.

Frequently asked questions

What’s the difference between an inactive stream and a sealed stream?

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.

Will deleting a stream affect consumers that reference it?

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.

How long should a stream be inactive before I consider deleting it?

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.

Can I recover a deleted stream?

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.

Do inactive streams affect cluster performance?

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.

Proactive monitoring for NATS inactive stream with Synadia Insights

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.

Start a 14-day Insights trial
Cancel