Repository navigation
bug(api): discovery endpoint timeout due to HTTP/2 request queuing on aggregated API server #2324
Description
Summary
The cozystack-api aggregated API server reports periodic Timeout or abort while handling GET /apis/apps.cozystack.io/v1alpha1 errors (~10-20 per minute). These timeouts cause the dashboard to intermittently return 500 errors and fail to load.
Root Cause
Discovery requests from system:kube-aggregator arrive at cozystack-api with already-expired deadlines. The requests are queued for ~30 seconds in the HTTP/2 transport layer before reaching the handler. Once delivered, cozystack-api processes them successfully in <1ms (HTTP 200), but the 30-second timeout has already fired.
Evidence from audit log
A single request showing both successful processing AND timeout:
RequestReceived: 2026-04-02T14:40:32.538478Z user=system:kube-aggregator src=10.80.4.150
ResponseComplete: 2026-04-02T14:40:32.539310Z code=200 (processing time: 0.8ms)
Timeout fired: 2026-04-02T14:40:32Z "Timeout or abort while handling"
The handler completed in 0.8ms with a 200 response, but the request's context deadline (set 30 seconds earlier by kube-apiserver) had already expired.
Why requests are delayed
The cozystack-api pod that receives dashboard watch streams also receives discovery probes from kube-apiserver. These share HTTP/2 connections from kube-apiserver's aggregation proxy. TCP socket analysis shows two connections from the same kube-apiserver to the affected pod:
- fd=13: healthy (rtt=0.07ms, 514KB transferred) — carries successful discovery requests
- fd=10: degraded (rtt=16ms, deviation=19ms, 30KB transferred) — likely carries delayed requests
The affected pod consistently has 27 active watch streams (one per ApplicationDefinition from dashboard BFF), while the unaffected pod has 0 watches. With 26 ApplicationDefinitions, each (*REST).Watch() creates a separate direct watch connection via client.WithWatch, generating significant HTTP/2 stream load through the kube-apiserver proxy.
Cascade to dashboard
- cozystack-api logs timeout errors (even though responses succeed)
- Some kube-apiserver discovery probes genuinely fail due to expired deadlines
- Dashboard BFF's swagger cache reload (2317 API paths) intermittently fails
- BFF becomes unresponsive → fails liveness probe (2s timeout) → killed → 500 for users
- Observed: 20 BFF container restarts over ~36 hours
Observations
- The problem always affects the pod receiving watch streams (consistent across multiple pod restarts and nodes)
- Direct access to the affected pod (curl from localhost) responds in 5ms — the pod itself is not slow
- Disabling Priority and Fairness (
--enable-priority-and-fairness=false) did not help - The forked
k8s.io/apimachineryonly changesscheme.go(GVK selection) and is unrelated - Each
Watch()call inpkg/registry/apps/application/rest.gocreates a new direct watch viar.w.Watch()instead of leveraging the shared controller-runtime informer cache (already watching HelmRelease inpkg/apiserver/apiserver.go:165). This creates 26+ individual watch streams per dashboard client session
Suggested Improvements
-
Use shared informer cache for Watch() instead of individual
client.WithWatchconnections. The controller-runtime manager already watches HelmRelease —Watch()could filter events from the shared cache rather than creating N separate watch streams. This would reduce HTTP/2 stream pressure from N×clients to 1 shared watch. -
Increase dashboard BFF liveness probe timeout from 2s to a higher value to reduce unnecessary restarts during transient API slowness.
Environment
- Cozystack v1.1.3
- Kubernetes v1.34 (Talos)
- 26 ApplicationDefinitions
- 2 cozystack-api replicas
- 3 control plane nodes
Activity
kvaps commentedon Apr 2, 2026
Additional findings: kube-controller-manager watch amplification
Further investigation via kube-apiserver audit logs on the node (using
talm) revealed that the watch stream count is significantly higher than initially estimated.Active watches through a single kube-apiserver (cp-150)
From the audit log, 36 active watch streams pass through kube-apiserver on cp-150:
system:serviceaccount:cozy-system:cozystack-api— these are the per-resource-type watches created by(*REST).Watch()going back into the same kube-apiserversystem:kube-controller-manager— metadata informers for allapps.cozystack.ioresource types (tenants, vpns, mongodbs, harbors, buckets, etc.)Watch amplification loop
Each
apps.cozystack.iowatch creates a circular dependency through the same kube-apiserver:Every incoming watch for an
apps.cozystack.ioresource spawns a new outgoing HelmRelease watch back to kube-apiserver. With 26 ApplicationDefinitions and multiple clients (kube-controller-manager on each node + dashboard BFF), this creates 50-80+ concurrent HTTP/2 streams funneled through a single kube-apiserver.Impact on HTTP/2 transport
The kube-apiserver audit log on cp-150 confirmed that all timeouts come from
system:kube-aggregator(the availability controller). The requests arrive at cozystack-api with already-expired 30s deadlines — they were queued in the HTTP/2 transport for the full timeout period before delivery.This reinforces that using the shared informer cache instead of individual
client.WithWatchconnections would eliminate the amplification loop entirely.dosubot commentedon Jul 27, 2026
Hi, Andrei Kvapil (@kvaps). I'm Dosu, and I'm helping the cozystack team manage their backlog and am marking this issue as stale.
Recap
system:kube-aggregator.client.WithWatchconnections with shared informer cache usage to remove the amplification loop.Next Steps
Thanks for your understanding and contribution.