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

A common community question is how to deliver configuration to thousands of services with NATS JetStream: each service has its own configuration, should receive the current value when it starts, and should continue receiving future updates.

There are two practical approaches:

  1. Use a NATS Key/Value bucket for the configuration.
  2. Use a JetStream stream directly with per-service filtering.

For most configuration use cases, start with NATS KV.

NATS KV is built on JetStream and is designed for “latest value by key” workloads. That maps naturally to service configuration:

  • Create a KV bucket for service configuration.
  • Store each service’s configuration under a key, such as svc_a, svc_b, or another naming scheme that fits your environment.
  • When a service starts, read its key with a KV get to obtain the current configuration.
  • Watch that key, or an appropriate key prefix, for future changes. A KV watcher also delivers the current value for each matching key before it streams live updates, so a watcher can cover both the initial read and ongoing changes. This is what makes “get the value on startup, then keep up with updates” straightforward with KV.

Conceptually:

1
KV bucket: service-config
2
3
key: svc_a -> configuration for service A
4
key: svc_b -> configuration for service B
5
key: svc_c -> configuration for service C

This is usually simpler than modeling each configuration value as a stream subject and managing consumers yourself. KV gives you a purpose-built abstraction for storing and observing values by key, while JetStream handles the persistence underneath.

Why KV instead of a stream with max_msgs_per_subject = 1?

A raw JetStream stream can represent this pattern. For example, you could publish service configurations on subjects such as:

1
config.svc_a
2
config.svc_b
3
config.svc_c

Then configure the stream to retain only the latest message per subject. That can work, and it gives you direct access to stream and consumer features.

However, if the requirement is simply “store the current configuration for this service and notify it when the value changes,” KV is the more direct API. It avoids making every application reason about stream retention, consumer cursors, subject filters, and delivery policies.

A useful way to think about the tradeoff:

RequirementPrefer
Latest config value by service IDKV
Watch config changes by key or prefixKV
Need low-level stream retention and consumer controlRaw JetStream stream
Need custom subject topology or stream processing behaviorRaw JetStream stream

KV is not a separate storage system from JetStream; it is a JetStream-backed abstraction. But that abstraction is important. Avoid depending on KV’s internal stream naming or implementation details as an application contract. Use the documented KV APIs when you choose KV.

Be careful with a single durable consumer

A common first design is to create one durable consumer and have all services use it to receive their own configuration updates.

That is usually not the right model.

A durable consumer represents a single logical delivery state. If many independent services share one durable, they are not getting independent per-service cursors. This can lead to surprising behavior and does not model “each service receives its own configuration stream” cleanly.

If you use raw JetStream streams, a better design is usually one filtered consumer per service instance or per service identity, depending on your delivery semantics. For example, a service interested only in config.svc_a can use a consumer filtered to that subject.

Are thousands of consumers acceptable?

Thousands of consumers may be workable, but they are not free. Consumers are server-side resources. Durable consumers in particular persist state and must be tracked even when the application is not actively connected.

If services are dynamic and only need configuration while they are running, ephemeral consumers are often a better fit than durable consumers. They avoid leaving long-lived consumer state behind for services that may not be active.

The tradeoff is operational:

  • Creating many ephemeral consumers at startup has overhead.
  • Durable consumers preserve state, but consume resources even while inactive.
  • The right answer depends on startup patterns, stream replication, storage choices, and failure semantics.

If you expect thousands of services to start at once, test that startup path under realistic conditions. Consumer creation, catch-up behavior, and configuration update fanout are all worth validating in your own environment.

Alternative: stream republish plus Core NATS subscriptions

Another design is to store configuration in JetStream and use stream republish so updates are emitted onto Core NATS subjects. Services can then subscribe with normal Core NATS subscriptions for ongoing updates.

This can reduce the need for a JetStream consumer per service for the live-update path. But it introduces another question: how does a service get the current value when it starts?

You still need a reliable initial read, such as:

This approach can be useful when you want lightweight live notifications, but it is less convenient than KV for the full “get current value, then watch for changes” lifecycle.

Practical recommendation

For per-service configuration, the most practical starting point is:

  1. Store configuration in a NATS KV bucket.
  2. Use one key per service, or a clear key hierarchy if services have multiple configuration documents.
  3. On service startup, read the current key value.
  4. Watch the key or prefix for future changes.
  5. Avoid a single shared durable consumer as the delivery mechanism for many independent services.

Use a raw JetStream stream when you need stream-specific controls that KV does not expose. In that case, prefer filtered consumers for the relevant service subject, and be intentional about durable versus ephemeral consumer lifecycle.

Summary

NATS KV is usually the right abstraction for service configuration because the problem is key-oriented: each service needs the latest value for its own key and a way to observe changes. JetStream streams can implement the same idea, but they require more consumer and retention design. If you choose the stream path, avoid sharing one durable consumer across independent services and validate the overhead of your consumer model at the scale you expect.


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