Watching for changes
Every collection you can list, you can also watch. Instead of asking repeatedly what changed, you open a stream, receive a snapshot of the current state, and then receive each change as it happens.
This matters more at the edge than in most systems. A fleet of ten thousand devices polled once a minute is ten thousand requests a minute to learn that almost nothing happened. The same information arrives over one watch stream, at the moment it becomes true.
Watch is a server-streaming call, so use gRPC. A REST watch endpoint exists, but a shell or browser client cannot consume a long-lived stream usefully.
The shape of a stream
A watch stream has two phases, and the transition between them is explicit.
- Snapshot — the current state of your query, possibly split across several responses.
- Incremental — one response per change, for as long as you stay connected.
The isCurrent flag marks the boundary. Accumulate responses while it is
false; when it is true, combine what you have and you hold a complete picture
as of the moment you asked. Every later response with isCurrent true
similarly completes an incremental batch.
So the client loop is always the same: buffer changes, and apply the buffer
when isCurrent arrives.
Each change carries a type — added, modified, removed, or current — and removals matter as much as additions. A device that no longer matches your filter is reported as removed even though it still exists, because it has left your query.
Choosing a watch type
There are two, and picking the wrong one is the main source of difficulty.
| Property | STATEFUL (default) |
STATELESS |
|---|---|---|
| Ordering and paging | Supported | Not supported |
| Resume after disconnect | Not supported | Supported, via a resume token |
| Result count | Capped by page size | Never capped |
| Reset and count handling | Handled by the server | Your responsibility |
| Intended for | UI views over a bounded page | Controllers and syncing over a whole collection |
The trade is between paging and resumption, and you cannot have both.
STATEFUL keeps per-session state on the server, which is what allows
ordering and page tokens. That state is discarded the moment the connection
drops, so there is nothing to resume against — a reconnect starts with a fresh
snapshot. In exchange, the server absorbs the hard parts: resets and count
reconciliation never reach you. Use it when you are backing a view that shows
a page of results in a defined order.
STATELESS keeps no session state, so it can stream a collection of any
size and hand you a resume token to reconnect with. The cost is that ordering
and paging are unavailable, and you must handle the reset and count signals
described below. Use it for anything that maintains a durable local view — a
controller, a cache, a synchronizer.
If you are writing a controller, you want STATELESS. If you are populating a
table in a UI, you want STATEFUL.
Chunking
Set maxChunkSize to bound how many changes arrive in a single response; the
maximum accepted is 100. For STATELESS watches the server applies its own
default if you do not choose one, so expect chunked responses either way and
rely on isCurrent, not on any response being complete.
Handling a STATELESS stream correctly
These three signals only appear on stateless watches, and ignoring them is how
a long-running integration drifts out of sync with reality. Each arrives on its
own — never alongside isCurrent or each other.
isSoftReset
The server hit a transient problem. Discard only the changes you have buffered
since the last isCurrent — your applied state is still good, and your resume
token is still valid.
buffered = [] # applied state and resume token untouched
isHardReset
The server cannot reconcile your view. Drop everything: your applied state and your resume token both. A fresh snapshot follows, and it arrives as changes of type current.
state = {}
resumeToken = None
buffered = []
snapshotSize
Occasionally the server sends a response with no changes and a snapshotSize
count, meaning “you should be holding this many resources”. Compare it with
your own count. A mismatch is not a warning to log — it means your view is
wrong, and the correct response is to reconnect and resnapshot. Ignore the
field when its value is negative, which means unpopulated.
This is a cheap consistency check that catches bugs in your own change application, so implement it even though nothing forces you to.
Resuming
Keep the most recent resumeToken — it arrives on responses where isCurrent
is true — and pass it on reconnect to continue from where you stopped instead
of resnapshotting the collection. Discard it on a hard reset.
Treat a resume token as opaque and short-lived. If the server rejects it, fall back to a fresh snapshot; that path will run eventually, so it needs to work.
Watching one resource
A single resource can be watched too, which is the efficient way to track one device’s status without polling it:
POST /v1/projects/{project}/regions/{region}/devices/{device}:watch
Filters on a watch
Watch accepts the same filter, view, and fieldMask controls as
reading resources, with one significant restriction: a single watch
request cannot express OR. As with list queries, AND composition is what
executes.
Where you need a union, open one watch per branch and merge the streams. The Go SDK includes a higher-level watcher that does exactly this — managing several concurrent queries, presenting one event channel, and hiding brief reconnections — so reach for that before building it yourself.
Practical advice
- Make change application idempotent. Resets and reconnects mean you will
occasionally apply the same change twice. Key your state by resource
nameand overwrite rather than append. - Use a field mask. A watch stream carries the same payload weight as a list response, on every change. If you only track connection status, ask only for connection status.
- Expect reconnections as routine. Deploys, rebalancing, and network blips all end streams. A watch client that treats a dropped connection as an error worth alerting on will alert constantly; reconnect with backoff instead.
- Do not mix polling and watching for the same data. Choose one source of truth, or you will spend time debugging which one was right.
Next steps
- Errors and limits — retry and backoff for streams as well as unary calls.