API rate limiting and pagination: how to handle both correctly
rate limiting headers, 429 Too Many Requests, Retry-After, exponential backoff, cursor pagination, offset pagination, Link header, page size
Rate Limiting and Pagination
Two patterns every API consumer must handle correctly: rate limiting protects the server from abuse, and pagination prevents downloading unbounded datasets in a single response.
Rate limiting headers
X-RateLimit-Limit: 1000 # Total requests allowed per window
X-RateLimit-Remaining: 47 # Requests left in the current window
X-RateLimit-Reset: 1710000300 # Unix timestamp when the window resets
Retry-After: 62 # Seconds to wait on a 429 responseWhen you receive a 429, read Retry-After and wait exactly that long. For general retry logic, use exponential backoff with jitter โ without jitter, all clients retry simultaneously after the window resets, creating a thundering herd problem:
// Exponential backoff with jitter
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));Pagination patterns
Offset pagination: GET /users?page=3&limit=20. Simple to implement but fragile โ inserting a record between page 2 and page 3 causes page 3 to skip or duplicate a record.
Cursor pagination: GET /users?after=user_abc&limit=20. The cursor is an opaque pointer (often an encoded ID or timestamp) to the last seen record. Stable under concurrent inserts and deletes. When next_cursor is null in the response, you have reached the last page. Most production APIs (GitHub, Stripe, Slack) use cursor pagination for large collections.
Always check for a null cursor before making another request โ continuing to paginate past null causes errors or duplicate data.
