Skip to content

Bug: Retry-After uses absolute Unix epoch on 429, instead of retry delay seconds #407

@lc0rp

Description

@lc0rp

Summary

When the API returns 429 Too Many Requests, the Retry-After header is generate from the absolute Unix epoch reset value, which is non-standard. The resulting large values returned can cause some clients to choke. If they treat it as delay-seconds, a value like 1771404540 means wait ~56 years (!)

Standards reference (RFC)

Evidence

1) Server code sets Retry-After to absolute reset epoch

In convex/lib/httpRateLimit.ts:121-127:

const resetSeconds = Math.ceil(result.resetAt / 1000)
return {
  'X-RateLimit-Limit': String(result.limit),
  'X-RateLimit-Remaining': String(result.remaining),
  'X-RateLimit-Reset': String(resetSeconds),
  ...(result.allowed ? {} : { 'Retry-After': String(resetSeconds) }),
}

result.resetAt is milliseconds since epoch. So Retry-After is emitted as epoch seconds (e.g. 1771404540), not a delay.

2) Live API confirms X-RateLimit-Reset is epoch seconds

Sample response from GET /api/v1/download?slug=gifgrep on 2026-02-18:

HTTP/2 200
x-ratelimit-limit: 20
x-ratelimit-remaining: 18
x-ratelimit-reset: 1771404540 <--- PROBLEMATIC

Expected behavior

On 429:

  • Retry-After should be a delay in seconds (ceil((resetAt - now)/1000)) or an HTTP-date.
  • Response should still include reset metadata (X-RateLimit-Reset or standardized RateLimit-* headers).
  • CLI should present actionable wait info to users.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions