Metadata and links
An object carries more than its bytes. So far invoice-ord_8w2k.pdf is only a
name and a payload: warehouse has to fetch the whole thing to learn
anything about it. This page adds metadata to the object: a human-readable
description, HTTP-style headers, and a free-form metadata map
that travel with the object. Then it teaches links, so one object can
stand in for another.
The invoices you stored on the previous pages are still in the INVOICES
bucket; keep that terminal open.
Every object carries an ObjectInfo
When you put an object, the store writes the chunks and then one final
metadata message describing the whole thing. That metadata message is an
ObjectInfo record. Some of its fields the store computes for you: the
byte size, the chunk count, the SHA-256 digest, the modification time, and
whether the object is deleted. Three fields are ones you set.
The first is the description: a single human-readable label for the object. The second is headers: HTTP-style key/value pairs, the same shape as the headers on a NATS message. The third is the metadata map: free-form key/value strings for whatever your application wants to record.
Set them on the put. order-svc stores the invoice with a description and a
content-type header so a reader knows the bytes are a PDF without fetching
them first:
- CLI
#!/bin/bash
# Put an invoice with metadata attached. Beyond the bytes, every object
# carries an ObjectInfo record. Three of its fields are yours to set:
# --description a human label for the object
# -H / --header HTTP-style headers (repeatable)
# and a free-form metadata map, set in the client libraries.
#
# Here `order-svc` stores invoice-ord_8w2k.pdf with a description and a
# content-type header so a reader knows the bytes are a PDF without
# downloading them first.
nats object put INVOICES invoice-ord_8w2k.pdf \
--description "Invoice for order ord_8w2k" \
--header "content-type:application/pdf"
# The metadata travels in the one metadata message that follows the
# chunks, so it costs nothing extra to read it back later — it is already
# beside the object, not in a separate lookup.
The object name and the bytes are unchanged: this is the same
invoice-ord_8w2k.pdf from your first object, now carrying a description and
a header. The metadata is held in that one trailing metadata message, so it
adds no extra storage cost and no extra read cost.
Reading the metadata back
Reading the metadata doesn't fetch the object's bytes. The store keeps the
latest ObjectInfo for each name as one small message, so warehouse can
read an object's details without reading the bytes.
- CLI
#!/bin/bash
# Read the ObjectInfo for one object. `nats object info` prints the
# metadata you set (description, headers, metadata map) alongside the
# fields the store computed for you: size, chunk count, the SHA-256
# digest, the modification time, and whether the object is deleted.
#
# `warehouse` reads this before fetching the bytes — the content-type
# header tells it the object is a PDF, and the size tells it how big the
# get will be.
nats object info INVOICES invoice-ord_8w2k.pdf
# Expected output (abridged):
#
# Object information for INVOICES > invoice-ord_8w2k.pdf
#
# Size: 12 KiB
# Modified: 2026-05-22 10:14:22
# Chunks: 1
# Digest: SHA-256=...
# Description: Invoice for order ord_8w2k
# Headers: content-type: application/pdf
The output shows your description and header next to the computed fields:
size, chunks, digest, modification time. warehouse reads the
content-type header and the size here, then decides whether to fetch the
bytes. The metadata is a low-cost index over the bucket, and you only read the
larger bytes when you need them.
There's one boundary to keep in mind. The metadata describes the current
object, not a history of past versions. Each re-put replaces the
metadata, keeping only the latest. If you want a full revision history per
name, that's the Key-Value store's job, covered in
Key-Value; the object store keeps the current
ObjectInfo, not the trail of edits that produced it.
Links point one object at another
A link is an object whose target is another object. A get on the link
transparently returns the target's bytes: you request the link, the store
follows it and returns the target. The link is a reference rather than a
copy: it stores no chunks of its own, only a record of the target's bucket
and name.
This is useful when two names should resolve to the same bytes. In the Acme
platform a shipping label and an invoice can share a document, or a stable
name can serve as the entry point for a frequently changing set of files. Here label-ord_8w2k.png becomes a
link to the invoice, so fetching the label returns the invoice's bytes:
- CLI
#!/bin/bash
# Add a link from one object to another. A link is an object whose target
# is another object (or a whole bucket); a get on the link transparently
# returns the target's bytes. Here `label-ord_8w2k.png` is a link that
# points at the invoice, so fetching the label hands back the invoice.
#
# Adding a link is a client-library operation (AddLink / AddBucketLink);
# the `nats` CLI has no link subcommand, so this snippet shows the shape
# with a client. The store records the target {bucket, name} at creation
# time, then traverses it on get.
#
# Pseudocode (the JS/Go/Python/Rust/Java/C# tabs show the real call):
#
# invoice = os.GetInfo("invoice-ord_8w2k.pdf")
# os.AddLink("label-ord_8w2k.png", invoice)
#
# A get on the link returns the invoice bytes, traversed for you:
nats object get INVOICES label-ord_8w2k.png --output ./label-target.pdf
# A bucket link points at a whole bucket instead of one object: pass an
# empty target name and the link resolves to the bucket on get. Reach for
# a bucket link when you want a stable alias for the store, not one file.
A link can also target a whole bucket instead of a single object. That's a bucket link: the target name is empty, and the link resolves to the bucket on get. Reach for a bucket link when you want a stable alias for the store as a whole rather than for one file inside it.
Two rules constrain links, and the store enforces both. A link can't point at a deleted object, and a link can't point at another link — the store won't build a chain of links you'd have to follow. When you add a link, the store records the target as it stands at that moment and traverses it on every get from then on.
The full set of ObjectInfo fields and link options is documented in
Reference. We only need the behavior here.
Pitfalls
Metadata and links have two pitfalls: what a link does when its target
moves, and what UpdateMeta will and won't change.
A link is a snapshot rather than a live reference. Adding a link records the
target's bucket and name at creation time; it doesn't keep the target alive.
Delete the target and the link is left dangling: a get on the link
traverses to a deleted object and fails with ErrObjectNotFound. The link
still exists, but its destination does not. Renames break a link the same way:
the link holds the old name, so renaming the target leaves the link
pointing at a name that no longer resolves. Do not assume addLink keeps the
target around or follows it under a rename. Do verify the target exists before
you depend on the link, or prefer a bucket link for loose coupling so a single
deleted or renamed object can't strand it.
You can see the failure and the safe check side by side. Delete the invoice,
get the now-stale label link, then confirm the target with info:
- CLI
#!/bin/bash
# A link is a snapshot of its target at creation time, not a live
# reference that keeps the target alive. Delete the target and the link
# is left dangling: a get on the link fails with a not-found error.
#
# Delete the invoice that label-ord_8w2k.png points at:
nats object rm INVOICES invoice-ord_8w2k.pdf --force
# Now get the link. The store traverses to invoice-ord_8w2k.pdf, finds it
# deleted, and fails — the link still exists, but its target does not:
#
# nats: error: nats: object not found
#
# In a client library this surfaces as ErrObjectNotFound. Always confirm
# the target exists before depending on a link, or prefer a bucket link
# for loose coupling so a single deleted object cannot strand it.
nats object get INVOICES label-ord_8w2k.png
# Confirm the target before you depend on the link. `nats object info`
# returns the metadata if the target is present and the same not-found
# error if it is gone:
nats object info INVOICES invoice-ord_8w2k.pdf
UpdateMeta changes the name, description, headers, and metadata, not the
chunk size or the link. Changing the name renames the object in place. If
you hand UpdateMeta a new chunk size or a new link target, those fields are
discarded without error or notification: the call succeeds, but neither is
stored. The chunk size is fixed when the bytes are written, and a link target
is fixed when the link is created. Don't expect UpdateMeta to re-chunk an
object or to change a link target. To change the chunk size, delete the object
and put it again. To change a link target, delete the link and add a new one.
And don't rename onto a name already in use: renaming an object to an
existing, non-deleted name fails with ErrObjectAlreadyExists. You can
rename onto a name that was deleted; that reclaims the name for the renamed
object.
Where you are
You now have:
invoice-ord_8w2k.pdfcarrying a description and acontent-typeheader, readable without fetching the bytes.label-ord_8w2k.pngas a link to the invoice, traversed transparently on get.- A working sense of the two link rules (no link to a deleted object, no
link to a link) and of what
UpdateMetadoes and doesn't touch.
The bucket now holds an object that carries its own metadata, and a link beside it. The next page moves from single objects to the whole bucket: how to take a snapshot of everything in it, and how to watch it change in real time.
What's next
The next page teaches list and watch: a snapshot of every object in
the bucket, and a live stream of metadata updates as analytics watches the
bucket fill.
Continue to Watching and listing.