Skip to content

bug(kiosk): console self-rate-limits on first boot (HTTP 429) causing card refresh failures #199

Open

Description

@castrojo

Bug Report

Symptoms

On first boot and initial dashboard hydration, several cards in the KubeStellar console report "Refresh failed" / "3 consecutive refresh failures", and API requests return 429 Too Many Requests.

Root Cause

The console's internal rate-limiting middleware (github.com/kubestellar/console/pkg/api/middleware/ratelimit.go) enforces a token bucket with strict burst caps. When the SPA boots, it fires simultaneous requests to:

  • /api/mcp/clusters
  • /api/mcp/services
  • /api/mcp/gpu-nodes/stream
  • /api/kagent/status
  • /api/stellar/state

The volume of simultaneous calls on first page load exhausts the bucket immediately, returning HTTP 429 and breaking initial card rendering.

Recommendation

Tune the rate-limiting burst threshold in the console deployment or configure the frontend router to stagger/batch initial API hydration requests.

Attribution

Assisted-by: github-copilot/gemini-3.8-flash via GitHub Copilot

Activity

  1. added
    hive/covered-by-prHive verified that an open PR references or claims this issue; still actionable until confirmed
    and removed
    hive/covered-by-prHive verified that an open PR references or claims this issue; still actionable until confirmed
    on Sep 25, 2026
  2. jeefy commented on Oct 3, 2026

    @jeefy
    Collaborator

    Still relevant after #358/#363: the Console is now the homelab kubestellar add-on (ghcr.io/kubestellar/console v0.3.42), and its first-load burst against the rate limiter is upstream Console behaviour. The fix belongs either upstream (stagger the SPA's initial hydration) or in the add-on's values (the rate-limit burst, if exposed). Leaving this open against the add-on.

  3. jeefy commented on Oct 6, 2026

    @jeefy
    Collaborator

    Status: root cause found. It's upstream. The Console has no setting for this limit, so I can't fix it in the add-on. Leaving this issue open.

    Root cause (Console v0.3.42 is the version we pin, and current main still has the same code)

    • pkg/api/routes_auth.go creates publicAPI := app.Group("/api", publicLimiterWithSkip). In Fiber v2, Group(prefix, handlers...) registers the handlers as Use middleware on the whole /api prefix. The "public" limiter therefore runs on every /api/* request, including authenticated ones. That limiter is a fixed window of 120 requests per minute per c.IP().
    • The authenticated limiter (2000 per minute, keyed by user and IP) and its skip list (for example /api/mcp/clusters) never get a say. The only skips that apply are /api/me, /api/version, /api/feedback/requests, /api/github/* and /api/auth/*.
    • Some public routes, such as /api/active-users, /api/youtube/playlist, /api/medium/blog and /api/public/nightly-e2e/runs, also name publicLimiter on the route itself. Those requests count twice.
    • One SPA load sends about 40 /api requests, about 35 of them in the first 2 seconds, and costs about 45–50 of the 120 budget. Two or three loads (reloads, tabs, a kiosk) inside one minute get 429s, which the cards report as refresh failures.
    • None of this can be configured. The limits are Go constants, and there is no env var, flag or config file for them. The trusted-proxy CIDRs are hardcoded too.

    Not a Gateway IP collapse. Envoy Gateway 1.9.2 runs with use_remote_address and appends the client address to X-Forwarded-For. Our Gateway Service uses externalTrafficPolicy: Local. The Console trusts XFF from RFC 1918 sources, which includes the Envoy pods, so each client gets its own bucket. (Side finding: EnableIPValidation is off, so c.IP() returns the raw XFF header. A client-supplied XFF therefore produces a fresh key. I'm reporting that upstream separately.)

    Evidence

    • Pinned image (ghcr.io/kubestellar/console:v0.3.42@sha256:8961…7717), run directly in podman. I fired the SPA's ~30 initial GETs in parallel, 5 rounds back to back:
      load1-3: 28×200 1×502 1×503 (the 502/503 come from having no cluster), load4: 15×200 15×429, load5: 30×429. After that, /api/mcp/clusters returns 429 (it is on the 2000/min skip list, so this is the 120/min limiter) and /api/me returns 200 (it is skipped).
    • Headless Chromium, one first load and 150 s on the dashboard: 42 /api requests in the first 40 s, then about 10/min steady.
    • QEMU dogfood-homelab-templates with an added probe, through the Gateway over HTTPS with the kc_auth session: PROBE cp-console-ratelimit secs=25 paths=30 … clusters-after=429 xff-spoofed=200,200,200, hdr=HTTP/2 429 retry-after: 60. Console log: 429 | GET /api/mcp/clusters, 429 | GET /api/health, … The Gateway Service shows LoadBalancer Local. All the usual template checks still PASS.

    Next: an upstream issue at kubestellar/console asking to scope publicLimiter to the public routes only (or to make the limits configurable). When a fixed Console release is out, bump the pin in scripts/render-homelab-manifests.py and close this.

  4. added
    triage/acceptedScope accepted; ready for work. Use `/triage accepted`.
    and removed on Oct 10, 2026
  5. Karibhou commented on Oct 20, 2026

    @Karibhou

    Summary: The dashboard can still reject its own requests when it first opens, leaving cards unable to refresh. This server project already uses the latest stable Console release, so there is no fixed version it can ship yet.


    The dependency is a KubeStellar Console fix that limits the public request cap to public routes, followed by a stable Console image and chart release. The server's scripts/render-homelab-manifests.py pins chart and image v0.3.42; v0.3.42 remains the latest stable Console release. The Console has no deployment setting for this limit, as documented in the root-cause investigation. Once a fixed stable release exists, update both pins and verify the first-load request pattern before closing this issue.

    — hive: backend=codex

  6. 33 remaining items

  7. added
    stage/010-actionableTriaged; eligible for the hive to pick up
    stage/020-claimedClaimed by the scanner or a relay contributor
    and removed
    blockedWaiting on something outside this repository. Use `/label blocked` or `/remove-label blocked`.
    stage/010-actionableTriaged; eligible for the hive to pick up
    on Oct 26, 2026
  8. hivecommons-hive commented on Oct 26, 2026

    @hivecommons-hive
    Contributor

    🛠️ Hive agent scanner (hive hosted-projectbluefin-knuckle-gjvq) started work on this issue at 2026-10-26T21:48:23Z. A pull request referencing this issue supersedes this mark; it lapses on its own at 2026-10-26T23:48:23Z if none appears.

  9. hivecommons-hive commented on Oct 26, 2026

    @hivecommons-hive
    Contributor

    Summary: I looked at fixing this from the server side and found nothing we can safely change. The request limit is hard-coded inside the Console itself, so the fix has to come from the Console project first.


    Re-checked against the root-cause comment above: the 120 req/min limiter is a Go constant attached to the whole /api prefix, with no env var, flag or chart value. Nothing in this repo (render script, manifests, Gateway config) can change it, and the latest stable Console is still v0.3.42. No PR opened; this stays open until an upstream release fixes it, then bump the pins in scripts/render-homelab-manifests.py. Releasing my claim is left to the hive.


    🐝 Hive Agent: scanner | Instance: hosted-projectbluefin-knuckle-gjvq | SHA: fcf0e78

    — hive: agent=scanner backend=copilot model=claude-opus-5.5 copilot=1.0.88

  10. added
    stage/010-actionableTriaged; eligible for the hive to pick up
    and removed
    stage/020-claimedClaimed by the scanner or a relay contributor
    on Oct 27, 2026
  11. hivecommons-hive commented on Oct 27, 2026

    @hivecommons-hive
    Contributor

    stage 020-claimed → 010-actionable: actionable

  12. kylerankin commented on Oct 27, 2026

    @kylerankin
    Contributor

    Summary: The dashboard on this project's homelab build can still reject its own requests when it first opens, leaving the on-screen cards unable to refresh until the limit resets a minute later. The cause lives inside the dashboard software this project ships (KubeStellar Console), not in this project's own code. This project only decides which version of that dashboard to include, and the corrected version has not been published yet, so there is nothing in this project to change right now. It stays blocked until the dashboard project fixes the request limit and ships a stable release containing that fix.


    • This server still pins the dashboard at chart/image v0.3.42 in scripts/render-homelab-manifests.py (lines 366 and 370) and at v0.3.34 in files/k0s/manifests/kubestellar/40-kubestellar-console.yaml.
    • The latest stable dashboard release is still v0.3.42 (published 2026-09-20); the only newer releases are v0.3.43-nightly.* pre-releases, which are not suitable as a production pin.
    • The dashboard main branch (commit 88bf46e) still hard-codes 120 requests per minute and attaches that limit to every /api request, including logged-in dashboard requests, with no setting, flag or environment variable to change it. This confirms the maintainer's root-cause investigation (bug(kiosk): console self-rate-limits on first boot (HTTP 429) causing card refresh failures #199, by jeefy).

    This is the external dependency identified in that investigation. Once a fixed stable dashboard release exists, update the pins in scripts/render-homelab-manifests.py (and the k0s manifest) and verify repeated dashboard loads before closing #199. No server files changed and no PR was opened.

    — hive: backend=pi model=lemonade/ornith-1.5-35b-a3b-gguf-q6_k


    🐝 Hive Agent: contributor | SHA: 3772407

  13. added
    blockedWaiting on something outside this repository. Use `/label blocked` or `/remove-label blocked`.
    stage/010-actionableTriaged; eligible for the hive to pick up
    and removed
    stage/010-actionableTriaged; eligible for the hive to pick up
    blockedWaiting on something outside this repository. Use `/label blocked` or `/remove-label blocked`.
    on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions