There is a particular feeling you get from a slow Kubernetes interface. You delete a pod, the row stays. You hit refresh, it is still there. You hit refresh again and now there are two pods, one terminating and one starting, and you are not sure which of those states is current and which is the interface catching up.
That feeling almost always has the same cause: the tool is polling. Understanding what it should be doing instead explains a lot about why some Kubernetes tools feel immediate and others feel like they are describing the recent past.
What polling actually costs
A polling client asks the same question on a timer:
GET /api/v1/namespaces/default/pods
GET /api/v1/namespaces/default/pods
GET /api/v1/namespaces/default/pods
Each request makes the API server read every matching object from etcd, serialise all of them, and send the complete set over the wire. The client then throws away the previous copy and renders the new one.
The cost scales with the number of objects and the number of clients, not with how much actually changed. A namespace with 400 pods that have not moved in an hour still transfers 400 pods every poll. Ten engineers with the dashboard open is ten times that. This is why cluster administrators have opinions about dashboards.
The latency is also structurally bad. With a five-second interval, the average change is displayed 2.5 seconds after it happened, and the worst case is five. Shortening the interval improves the latency and makes the load problem worse in exact proportion. There is no interval that is both fast and cheap, because the mechanism is wrong.
The list-and-watch protocol
Kubernetes provides a better primitive, and it is the same one every controller in the system uses.
The client starts with one list:
GET /api/v1/namespaces/default/pods
The response includes a resourceVersion on the list itself. That value is a position in the cluster's change stream — a bookmark saying "this is the state of the world as of here".
The client then opens a watch starting from that position:
GET /api/v1/namespaces/default/pods?watch=true&resourceVersion=41827
This request does not return. It stays open and the API server writes an event into it every time a matching object changes:
{"type":"MODIFIED","object":{"kind":"Pod","metadata":{"name":"api-7d9f","resourceVersion":"41831"},...}}
{"type":"DELETED","object":{"kind":"Pod","metadata":{"name":"api-6c2a","resourceVersion":"41832"},...}}
{"type":"ADDED","object":{"kind":"Pod","metadata":{"name":"api-9f1b","resourceVersion":"41833"},...}}
Three properties follow from this, and they are the entire difference.
Traffic is proportional to change. A quiet namespace costs nothing after the initial list, no matter how many objects it contains or how long you leave the window open.
Latency is proportional to nothing. The event is written when the change is committed. There is no interval to wait out.
Nothing is missed. Because every event carries a resourceVersion and the stream is ordered, the client always knows exactly where it is.
Why watches expire, and what to do about it
A watch is not permanent. The API server will close it, and there are two distinct reasons that need different handling.
The mundane one is that the server closes idle or long-lived connections deliberately, to spread load when API server instances come and go. The client reconnects from the last resourceVersion it saw and continues without a gap.
The interesting one is 410 Gone. The API server keeps a limited window of change history — etcd compacts old revisions, and the watch cache holds a bounded number of recent events. If your client is offline long enough that its resourceVersion falls out of that window, the server can no longer tell you what you missed. It cannot send you the events, because they are gone.
The only correct response is to list again from scratch and resynchronise:
410 Gone -> GET /api/v1/.../pods -> watch from the new resourceVersion
A client that handles 410 by simply reconnecting from the same stale version gets 410 again, forever, and quietly stops updating while continuing to look like it is working. This is a real and common bug, and it produces exactly the symptom of a view that was correct when you opened it and has been drifting ever since.
There is also a BOOKMARK event type, which exists specifically for this problem. The server periodically sends an event with no object payload, just a current resourceVersion, so that a client watching a quiet resource keeps its position fresh instead of falling behind the compaction window while nothing happens. A client that requests bookmarks with allowWatchBookmarks=true is much harder to strand.
What an informer adds
In practice, clients do not implement list-and-watch by hand. The Kubernetes client libraries provide an informer, which wraps the protocol and adds a local cache.
The informer holds every object of its resource type in memory, updated by the watch stream. That cache is what your interface reads from. It has three consequences worth naming.
Reads become free. Filtering, sorting and searching happen against local memory. Typing in a filter box does not generate API traffic, so it can be genuinely instant rather than debounced-and-hopeful.
The full object is available. A watch event carries the entire object, not a diff, so the cache always has complete objects and any view can be rendered without going back to the API server for detail.
Reconnection is handled once. Expiry, 410, resync and backoff live in the informer rather than being reimplemented, differently and incompletely, in each part of the application.
The cost is memory, and it is proportional to the number of objects being watched. This is a real constraint at scale, which is why watching every resource kind in every namespace at once is not a sensible default. Watching what is currently on screen is.
Where this shows up in the interface
The architecture is visible from the outside once you know what to look for.
A watch-backed view has no refresh button, because a refresh button would do nothing that is not already happening. When you delete a pod, the row disappears when the deletion is committed. A pod that enters CrashLoopBackOff at 14:02 changes colour at 14:02, not on the next tick.
A watch-backed view can also show transient states at all. Pods pass through Pending and ContainerCreating in a few seconds. A five-second poll will frequently step straight over them, so failures during startup appear as an object that was fine and is now broken, with the intermediate state that would have explained why never displayed.
And a watch-backed view stays honest when you leave it open. The reason 410 handling matters is that the failure it causes is invisible: nothing errors, the view simply stops changing. If you have ever left a dashboard open over lunch and come back to a cluster state that turned out to be an hour old, you have probably seen it.
Why this is harder in a multi-cluster tool
Everything above describes one client watching one cluster. A tool that shows several clusters at once has to do all of it per cluster, in parallel, and the failure modes stop being independent.
Each cluster needs its own informers and its own reconnection state. A cluster that becomes unreachable — a VPN dropping, most often — must not stall the views for the others. Credentials expire at different times, because they came from different identity providers. And a cluster you switch away from should have its watches released rather than left running, or the memory cost grows with every cluster you have visited rather than every cluster you are using.
None of that is conceptually difficult, but it is a meaningful amount of state to get right, and it is the reason that many otherwise good single-cluster tools feel fragile the moment you point them at a dozen clusters belonging to a dozen organisations.
Biebie Kube is built on watch-backed resource views across every cluster you have configured, which is what makes its tables update in place rather than on a timer. If you want the operational patterns that go with it, the article on managing Kubernetes clusters for multiple customers covers kubeconfig layout, context naming and production safety.