Rate Limits & Quotas
GKit SheetsAPI rate limits by plan, how to read rate-limit headers, handle 429 errors, implement backoff, and design for quota efficiency.
SheetsAPI enforces per-plan rate limits to ensure fair use across all accounts. Every response includes headers that reflect your current quota position so you can react before hitting a limit rather than after.
Limits by plan
Two independent limits apply to every API key: a per-second burst limit and a daily quota. Both are enforced separately - exhausting one blocks further requests even if the other has headroom remaining.
| Plan | Requests / second | Daily quota |
|---|---|---|
| Free | 10 | 5,000 req / day |
| Pro | 100 | 500,000 req / day |
| Enterprise | Custom | Custom |
The burst window resets every rolling second. The daily quota resets at midnight UTC.
Enterprise limits are negotiated per customer. Contact sales@matters.ai to discuss sustained throughput requirements.
Rate-limit response headers
Every API response includes three headers that reflect the state of your current burst-rate window:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current one-second window for your plan. |
X-RateLimit-Remaining | Requests still available in the current window. |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets and Remaining refills. |
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1751234161
Read X-RateLimit-Remaining proactively in high-throughput code. If it reaches zero,
pause until X-RateLimit-Reset before sending the next request.
429 response body
When you exceed either limit the API responds with HTTP 429 Too Many Requests. The
JSON body identifies which limit was hit and how long to wait:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Request rate exceeded. Retry after 1 second.",
"limit_type": "per_second",
"retry_after": 1
}
}limit_type is either "per_second" or "daily". For daily quota exhaustion,
retry_after reflects seconds until midnight UTC rather than the next window
boundary.
Exponential backoff
Retrying immediately after a 429 typically triggers another 429. Use truncated exponential backoff with jitter to spread load and avoid thundering-herd conditions when multiple clients retry at the same time.
// Node.js - exponential backoff with jitter
async function fetchWithBackoff(url, options = {}, maxRetries = 5) {
const BASE_DELAY_MS = 500;
const MAX_DELAY_MS = 30_000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response; // success or a non-rate-limit error
}
if (attempt === maxRetries) {
throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
}
// Honour the server-provided wait time when present
const retryAfterHeader = response.headers.get("Retry-After");
const serverDelay = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1000 : 0;
const exponentialDelay = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
const jitter = Math.random() * exponentialDelay * 0.3;
const delay = Math.max(serverDelay, exponentialDelay + jitter);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}Key points:
- Start with a 500 ms base delay and double on each retry.
- Cap at 30 seconds to avoid indefinitely long waits on daily quota exhaustion.
- Add up to 30% random jitter so concurrent clients do not retry in lockstep.
- Always honour the
Retry-Afterresponse header - it may be shorter than your computed backoff for per-second limits.
Quota efficiency tips
Staying within limits is as much about request design as it is about retry logic.
Paginate instead of full-fetching. Retrieve only the rows your UI needs right now.
Pass limit and offset to walk through large sheets in manageable chunks and stop
early when you have found the target record.
GET /v1/sheets/{sheetId}/rows?limit=200&offset=0See Pagination for a complete walkthrough.
Project only the fields you need. Use the fields parameter to restrict the
response to specific columns. Smaller payloads parse faster and reduce the chance of
timeout-induced retries.
GET /v1/sheets/{sheetId}/rows?fields=id,name,status&limit=200Cache read responses. Reference data that changes infrequently - lookup tables, configuration rows, category lists - should be cached client-side and revalidated on a schedule rather than fetched on every render. Even a 60-second cache on a high-traffic endpoint can cut daily quota consumption by an order of magnitude.
// Next.js - revalidate every 60 seconds
fetch(url, { next: { revalidate: 60 } });See Caching Strategies for more patterns.
Batch writes. The rows.batchUpdate endpoint accepts up to 500 row mutations in a
single request. Prefer one batch call over hundreds of individual writes when
processing bulk data.
Monitor X-RateLimit-Remaining in production. Log or alert when remaining falls
below 20% of the limit on a sustained basis. Consistent proximity to the ceiling is a
signal to upgrade your plan or restructure your access pattern before 429 errors
appear in production.