This document outlines the implementation and testing process for adding adaptive, rate-limit-aware request queuing to the GuildPass SDK, addressing issue #305.
The GuildPass SDK lacked awareness of API rate limits, specifically 429 (Too Many Requests) responses and the Retry-After header. This could lead to applications making many concurrent calls triggering 429s, which were treated as generic failures instead of being intelligently paced and retried.
The internal HTTP layer now recognizes 429 responses, honors any Retry-After header (or a sensible default backoff if absent), and queues/paces subsequent outgoing requests to avoid immediately re-triggering the same rate limit. This behavior is distinct from and coordinates with the circuit breaker. The current throttling state is also exposed via an optional diagnostics method.
-
Analyze Existing HTTP Layer:
- Examined
src/http/httpClient.tsto understand request/response handling and existing retry mechanisms. - Identified the
getRetryAfterMsfunction for parsingRetry-Afterheaders and theTokenBucketintegration. - Noted that the existing retry logic for 429s directly delayed the current request, but a global pacing mechanism was needed.
- Examined
-
Enhance
TokenBucket(src/http/tokenBucket.ts):- Added
retryUntilproperty: A privateretryUntil: number = 0;was introduced to store the timestamp until which all requests should be throttled. - Updated
onRateLimited: When aretryAfterMsis provided (from a 429 response),retryUntilis set toDate.now() + retryAfterMs. This ensures a hard cool-down period. The existing rate adjustment logic was retained for adaptive pacing after the hard throttle. - Modified
acquire: Theacquiremethod was updated to first checkretryUntil. IfDate.now()is less thanretryUntil, itawaits the remaining duration, effectively blocking all new requests until the cool-down period ends. - Added
getThrottlingUntil: A public method was added to allow external components (e.g., for diagnostics or testing) to query theretryUntiltimestamp.
- Added
-
Adjust
HttpClient(src/http/httpClient.ts):- The direct
await delay(retryAfter ?? backoff);call within the 429 handling block was removed. The responsibility for delaying requests based onRetry-Afteris now fully delegated to thetokenBucket.acquire()method, which applies the cool-down globally.
- The direct
-
Verify
http.types.ts:- Confirmed that
src/http/http.types.tsalready correctly definesRateLimitConfigand includesrateLimit?: RateLimitConfig;withinHttpClientConfig, meaning no changes were required here.
- Confirmed that
Tests were added and modified in tests/httpClient.test.ts using vitest's fake timers (vi.useFakeTimers(), vi.advanceTimersByTime()) to accurately simulate time-dependent behavior.
-
respects Retry-After header on 429(Modified):- Updated to use a realistic
Retry-Aftervalue (e.g., 100ms). - Assertions were added to confirm that the retried request is delayed by at least the specified
Retry-Afterduration.
- Updated to use a realistic
-
paces concurrent requests after a 429 with Retry-After(New Test):- Simulates a scenario where a 429 is received, followed by a burst of concurrent requests.
- Verifies that all subsequent requests are correctly held back and then released after the
Retry-Afterperiod has elapsed, demonstrating the global pacing mechanism.
-
exposes throttling state via getThrottlingUntil(New Test):- Tests the
getThrottlingUntil()method of theTokenBucket(accessed throughHttpClient). - Asserts that
getThrottlingUntil()accurately reflects the active throttling period after a 429 and resets once the cool-down is over.
- Tests the
- A test simulating a 429 response with a
Retry-Afterheader results in the retried request being delayed by (at least) that duration before firing. - A burst of many concurrent calls following a detected rate limit is demonstrably paced (verified via timing assertions in a test) rather than all firing immediately.
- Behavior is clearly distinguished from and doesn't conflict with the circuit breaker's failure-based tripping (the
TokenBuckethandles pacing, while the circuit breaker would handle outright, unrecoverable failures).
This implementation provides a robust and adaptive rate-limiting solution for the GuildPass SDK, significantly improving its resilience and behavior under high load or API restrictions.