This episode covers two of JetStream's built-in stores: a key-value store with TTL, history, watchers, and revision-based updates, plus an object store for storing large files in chunks that integrates with application data flows.

Up to episode 9, NATS was only transporting messages. Episode 10 reveals a side of NATS few people know: it also provides data storage directly on top of JetStream — a key-value store for small, fast data, and an object store for large files.
Both free you from additional storage systems for many simple cases. Let's break down KV buckets, then the object store.
The NATS key-value store lives in buckets. Create a bucket, then set and get values:
nats kv add config
nats kv put config app.name "devvnull"
nats kv get config app.namenats kv add config creates a bucket named config, nats kv put config app.name "devvnull" stores a value, and nats kv get config app.name retrieves it back. Behind the scenes, a KV bucket is just a stream with Limits retention and one subject per key.
Keys support dotted patterns so they can be organized:
nats kv put config db.host "postgres"
nats kv put config db.port "5432"
nats kv ls confignats kv ls config lists all keys. The config.db.host structure allows grouping keys by domain — just like the subject hierarchy discussed in episode 4.
Each key can be given a time-to-live (TTL):
nats kv put config session.token "abc123" --ttl=1h--ttl=1h makes the session.token key automatically disappear after one hour. TTL is very useful for caches and temporary tokens without needing a cleanup cron.
A KV bucket keeps the history of every key:
nats kv history config db.hostnats kv history config db.host shows every version of the key along with its revision number. This history is what enables safe update operations, which we'll discuss shortly.
A watcher notifies the application every time a key changes:
import asyncio
import nats
async def main():
nc = await nats.connect("nats://localhost:4222")
js = nc.jetstream()
kv = await js.create_key_value(bucket="config")
async for entry in kv.watch("db.host"):
print("perubahan:", entry.key, entry.value)
asyncio.run(main())kv.watch("db.host") produces a real-time stream of changes. This KV-plus-watcher combination is the basis of the config propagation pattern: one service changes a value, all other services know immediately.
When several services write the same key, you need control so updates don't overwrite each other. NATS provides revisions: every write operation has a version number, and updates can be conditioned on a specific revision.
entry = await kv.get("db.host")
await kv.update("db.host", b"postgres-v2", revision=entry.revision)kv.update("db.host", b"postgres-v2", revision=entry.revision) only succeeds if the current revision still matches — if another service wrote first, the operation fails. This prevents lost updates and becomes the foundation of the idempotency we'll use in episode 11.
Info
The read-modify-write pattern with revisions is how NATS provides compare-and-swap guarantees. For counters or shared state between services, always use this revision guard — never write directly without a check.
The object store stores large files — images, videos, artifacts — as chunks saved as JetStream messages. The advantages: distributed storage, replication, and access from anywhere in the cluster.
nats object add assets
nats object put assets logo.png ./logo.png
nats object get assets logo.png ./download.pngnats object add assets creates an object bucket, nats object put assets logo.png ./logo.png uploads a file, and nats object get downloads it back. Client libraries also provide the same API for each language.
Every object stores complete metadata:
nats object info assets logo.png
nats object ls assetsnats object info assets logo.png shows the size, chunk count, and upload time. nats object ls assets lists all files in the bucket. This object store is useful for keeping process results, models, or build artifacts inside the NATS ecosystem.
JetStream's three stores complement each other in a single flow:
publisher --> stream ORDERS --> worker processes
├── status in KV bucket
└── result file in object storeThe status in KV bucket pattern lets every worker read the latest status without querying the stream, while large result files are stored in the object store. It's a compact architecture: one NATS server handles messages, state, and files at the same time.
Episode 10 introduced two of NATS's built-in stores: a key-value store with buckets, TTL, history, watchers, and revision-based updates for concurrency safety, plus an object store that keeps large files as distributed chunks. Both run on top of JetStream with no additional components.
Key takeaways:
nats kv add and accessed with put, get, and ls.In episode 11 next, we'll discuss stream manager & work queues — building job queues with a WorkQueue stream and pull consumers, distributing unique messages to workers, and achieving exactly-once and idempotency through publisher dedupe windows, idempotency keys, and synchronization with KV or a database. This pattern is the backbone of many production systems.