Skip to content

fix(sdk): treat a stream 429 as retryable and honor Retry-After #469

Description

@EricAndrechek

Area: sdk — streaming reliability. Surfaced in review of #203, deliberately not changed there.

The fetch-based stream transport treats any 4xx as terminal: parseErrorResponse (clients/ts/src/errors.ts) marks only >= 500 retryable, and _attempt ends the stream on a non-retryable status. That rule is right for the cases it was written for — a rejected token or a missing table will not be talked round by repeating the request — and it matches the REST path.

429 Too Many Requests is the exception. It is explicitly a retry-later status, usually carrying Retry-After. WaveHouse itself never emits it on /v1/stream (the endpoint is ungated), so it comes from a fronting proxy, CDN, or API gateway — exactly the deployments this transport is meant to work behind.

The failure mode is unpleasant: a brief upstream blip drops many streams at once, they all reconnect, the proxy rate-limits the burst, and every client takes a terminal HTTP_429 and stays dead until the page reloads or the process restarts. The jittered backoff added in #203 exists to avoid exactly that thundering herd, and a terminal 429 defeats it at the last step.

Proposal

Treat 429 as retryable on the stream path, and honor Retry-After as a backoff floor — the mechanism already exists (_retryFloorMs, fed today only by an SSE retry: field, clamped to MAX_BACKOFF_MS).

  • Parse Retry-After in both forms: delta-seconds and an HTTP-date.
  • Clamp as retry: already is, so a hostile or fat-fingered value can't strand a stream.
  • Keep every other 4xx terminal.

The REST path already special-cases 503 + Retry-After (clients/ts/src/http.ts), so this is consistent with how the SDK treats the other "come back later" status rather than a new concept.

Worth deciding explicitly

Whether REST should get the same treatment for 429. Today a REST 429 is non-retryable too. Arguably the same argument applies, but the blast radius differs: a REST call returns an error the caller can act on, whereas a dead stream is silent until someone notices data stopped arriving.

Acceptance

  • 429 on a stream is retryable and re-dials
  • Retry-After (seconds and HTTP-date) sets the backoff floor, clamped to 30s
  • Every other 4xx stays terminal — pinned by a test, since this is the rule protecting against retrying a rejected token forever
  • Docs: the error table in docs/src/content/docs/sdk/reference.md and the "4xx is terminal" prose in sdk/streaming.md

Related: #203 (introduced the terminal-4xx rule and the backoff), #465 (the other streaming-load issue).

Activity

  1. coderabbitai commented on Aug 13, 2026

    @coderabbitai
    ⚠️ Possible Duplicate Issue(s)
    🔗 Related PRs

    #174 - refactor(table names): handle unsafe table names [closed]
    #313 - feat(docs): view-transition polish, branded search + 404, chrome pass [closed]
    #448 - fix(sdk): keep a baseURL path prefix instead of discarding it [merged]
    #456 - feat(sdk)!: add options.headers, fetchOptions, and fetch [merged]


    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/sdkTypeScript SDK (clients/ts/)area/streamingSSE / live-query delivery path (/v1/stream)enhancementNew feature or request

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions