Measure the watch path before enabling broader scopes or sharing: upstream watch count, downstream bytes/events, snapshot size/time, subscriber authorization and browser notifications/renders. Suppression reduces downstream work; sharing reduces duplicate upstream watches but retains per-subscriber costs. See watching resources for those boundaries.
The gateway reports low-cardinality lifecycle signals through Observer, so hosts can count them
without inspecting resources:
metrics := gateway.ObserverFunc(func(o gateway.Observation) {
streamsTotal.WithLabelValues(string(o.Kind), string(o.Projection), string(o.EventType)).Inc()
})Do not block in Observe; it runs on the stream or shared-watch goroutine. Never label a metric with
an object name, UID, principal, patch contents, scope value or error message.
Error text goes to StreamConfig.Diagnostics instead, because it can name internal addresses and URLs; the
browser receives only the protocol code and a message chosen for it. Redact before logging.
| Signal | Meaning | First response |
|---|---|---|
http_transport_rejected |
bounded HTTP serving lacks required writer capabilities; no logical stream opened | test the mounted middleware and provide flush/deadline support; aborted requests may reconnect |
consumer_resync rising |
upstream continuity was lost or a shared subscriber fell behind | correlate with API-server errors, shared overflows, and deployments |
shared_overflow |
one subscriber exceeded SharedOptions.QueueDepth |
increase only after checking browser stalls and event rate; resnapshot is intentional |
terminal_error |
logical-stream failure, observed before attempting its terminal frame; delivery can fail | alert by low-cardinality error code; browsers must not retry terminal errors |
retryable_error rising |
streams ending with a non-terminal error, usually UPSTREAM_UNAVAILABLE |
check API-server health and priority-and-fairness rejections; browsers reconnect on their own backoff |
event_suppressed ratio |
the selected projection is removing unchanged-view churn | a sharp drop may mean callers selected krm-full/v1 or a projection changed |
| stream count / snapshot duration | connection pressure or oversized scopes | narrow namespaces/selectors; avoid accidental all-namespaces watches |
unorderable resourceVersion terminal errors |
an unsupported or aggregated API does not meet strict ordering | use OrderingLenient only after accepting the reduced monotonicity guarantee |
gateway/kube maps API-server answers to protocol codes:
| API server | Browser receives |
|---|---|
| 403 | FORBIDDEN, with Kubernetes' message |
| 401 | UNAUTHENTICATED |
| 404 | SCOPE_INVALID |
| 429, 5xx, unreachable | UPSTREAM_UNAVAILABLE, with Retry-After as retryAfterMs; the connection closes and the client retries |
- Streams stuck in
syncingwithout errors. client-go retries a 429 or 5xx that carriesRetry-Afterup to ten times before the gateway sees it, and the browser waits with heartbeats meanwhile. Look at API-server load. retryable_errorfrom watches that end early. A watch that ends before its snapshot completes, or within a second of it, is recovered once on the same connection; a second in a row closes the connection withUPSTREAM_UNAVAILABLE. Look for an upstream that ends every watch.- One shared scope slow to open. It holds up only the streams waiting for that scope, and each ends with its request. A client-go watch has no response-header timeout of its own, so look at that scope's API server.
- A shared scope refused with
retryAfterMswhile others work.SharedBackendbacks off a scope whose upstream will not open, so its subscribers' reconnects do not stampede.
| Control | Default | Use |
|---|---|---|
StreamConfig.WriteTimeout |
0 (no deadline) | set a positive per-operation write-plus-flush budget for bounded HTTP delivery; required with ReauthorizationInterval |
StreamConfig.ReauthorizationInterval |
0 (cycle checks only) | recheck each subscriber on quiet streams; see the revocation budget |
StreamConfig.ReauthorizationTimeout |
10 seconds | bound each periodic check's callbacks once it holds the delivery gate |
StreamConfig.HeartbeatInterval |
20 seconds | set below the shortest proxy idle timeout |
gateway.SharedOptions.QueueDepth |
256 live events | tune after measuring; it bounds memory per slow subscriber |
ScopePolicy.AllowLabelSelector |
false | enable only for an endpoint that deliberately supports caller narrowing |
GroupResource.AllowAllNamespaces |
false | make all-namespaces access an explicit reviewable policy decision |
StreamConfig.Ordering |
strict | keep strict on supported Kubernetes; use lenient only for known aggregated APIs |
StreamConfig.Diagnostics |
nil (discarded) | receive the raw error behind each error event; redact before logging |
The gateway sets no snapshot object or byte limit: what is safe depends on the product. Measure snapshot size and duration per allowed scope before opening broad all-namespaces endpoints.
stream_opened/stream_closed count Stream entry/return, including authorization
failure, but exclude HTTP identity/scope refusals before entry. shared_subscription_opened and
shared_subscription_closed count active attachments, including warm-cache joins. Overflow, scope
death or leave ends an attachment once; repeated Stop does not count again. A resnapshot can
replace an attachment on one logical stream without another API watch.
Callbacks are synchronous and may run concurrently, including under shared locks. Return promptly; do not panic or reenter the gateway. Update gauges synchronously rather than through a lossy exporter queue. Opens precede matching closes, with no global order across lifetimes. Ignore unknown kinds. See the tested counter mapping.
These gauges do not count HTTP requests still resolving identity, or upstream watch handles. Measure
physical API-server WATCH requests separately; the shared-host fixture uses
apiserver_longrunning_requests filtered to the resource and verb="WATCH".
Set WriteTimeout explicitly. It is required with timed reauthorization: Handler panics at
construction without it, and ServeStream panics before writing. Each header, event or heartbeat
write and its flush share one deadline, cleared after success, so a quiet stream outlives many
timeout periods. A failed write ends its stream. Flush success does not acknowledge browser receipt.
The library owns write deadlines while serving and clears them at exit; it cannot restore a host's
previous deadline. A whole-response http.Server.WriteTimeout is no substitute on a long-lived
stream. The bound covers I/O only, not backend opening, gate waiting, encoding or host callbacks. A
generic Stream sink is the host's to bound.
With a positive bound, an unsupported writer aborts before streaming and emits
http_transport_rejected. Test your mounted middleware with the
capability-check recipe.
HTTP/1.1, TLS HTTP/2 (including two streams on one connection), and transparent and opaque wrappers
are tested; other middleware and proxy combinations need host tests.
For authorization bursts and the limits of the 200-subscriber profile, see the shared-host capacity guide.
Upstream continuation, native browser watch recovery and save-progress evaluation are planned work, not current runtime controls. When measuring editing, distinguish stale-version 409s from field conflicts and include save latency and successful saves during sustained controller churn, rather than only after it stops.