Notion API Rate Limit & 502 Errors: Complete Troubleshooting Guide
Fix Notion API rate limit (429) and 502 Bad Gateway errors with exponential backoff, request queuing, and retry logic. Step-by-step guide with code examples.
- Notion enforces a hard rate limit of 3 requests per second per integration token, returning HTTP 429 with a Retry-After header when exceeded
- HTTP 502 Bad Gateway from Notion's API indicates a transient upstream failure on Notion's infrastructure, not a client-side misconfiguration
- Implement exponential backoff with jitter on both 429 and 502 responses — start at 1 second, cap at 64 seconds, and respect the Retry-After header value when present
| Method | When to Use | Time to Implement | Risk |
|---|---|---|---|
| Respect Retry-After header | First occurrence of 429, single-threaded client | < 30 min | Low |
| Exponential backoff with jitter | Repeated 429/502 under sustained load | 1–2 hours | Low |
| Token bucket / request queue | High-throughput integrations (>100 req/min) | 2–4 hours | Medium |
| Multiple integration tokens | Separate workspaces or parallel pipelines | 1 hour | Medium — Notion ToS applies |
| Batch / bulk endpoints | Reading multiple pages/blocks in one call | 2–3 hours | Low |
| Client-side caching layer | Repeatedly reading the same page data | 2–4 hours | Low |
| Webhooks instead of polling | Event-driven pipelines that currently poll | 4–8 hours | Low — reduces request volume significantly |
Understanding Notion API Rate Limits and 502 Errors
Notion's public API enforces rate limits at the integration level. As of 2024, the documented limit is 3 requests per second per integration token (also stated as an average rate with burst tolerance). Exceeding this threshold causes Notion to return:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
content-type: application/json
{"object":"error","status":429,"code":"rate_limited","message":"You have been rate limited. Please try again later."}
A 502 Bad Gateway is a different class of error. It originates from Notion's infrastructure, typically a timeout or failure between their edge proxy and backend services. The response body is often an HTML error page or an empty body rather than Notion's standard JSON error envelope:
HTTP/1.1 502 Bad Gateway
content-type: text/html
<html><body><h1>502 Bad Gateway</h1></body></html>
Both errors are retriable — 429 after the specified delay, 502 after a short wait. Neither indicates a permanent problem with your API key or workspace configuration.
Step 1: Diagnose the Exact Error
Before writing any retry logic, confirm what you are actually receiving.
Check HTTP status code and headers:
Run a raw request with curl to inspect headers and body without any client library abstraction:
curl -i -X GET 'https://api.notion.com/v1/pages/YOUR_PAGE_ID' \
-H 'Authorization: Bearer secret_YOUR_TOKEN' \
-H 'Notion-Version: 2022-06-28'
The -i flag prints response headers. Look for:
HTTP/2 429→ you are rate limitedHTTP/2 502→ Notion-side transient failureRetry-After: N→ seconds to wait before retrying (present on 429, sometimes on 502)x-request-id→ copy this value; Notion support needs it for 502 investigations
Confirm your request rate:
If you are running a script, add temporary logging to count requests per second before assuming a bug:
import time
from collections import deque
request_timestamps = deque()
def log_request():
now = time.monotonic()
request_timestamps.append(now)
# Drop timestamps older than 1 second
while request_timestamps and request_timestamps[0] < now - 1:
request_timestamps.popleft()
print(f"Requests in last second: {len(request_timestamps)}")
If this logs values above 3, you are the source of the 429s.
Step 2: Implement Exponential Backoff with Jitter
The most reliable fix for both 429 and 502 is a retry wrapper that respects back-pressure signals from the server.
Python example using requests:
import time
import random
import requests
def notion_request_with_retry(method, url, max_retries=5, **kwargs):
base_delay = 1.0
for attempt in range(max_retries):
response = requests.request(method, url, **kwargs)
if response.status_code == 429:
retry_after = float(response.headers.get('Retry-After', base_delay * (2 ** attempt)))
jitter = random.uniform(0, 0.5)
sleep_time = retry_after + jitter
print(f"Rate limited (429). Waiting {sleep_time:.2f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(sleep_time)
continue
if response.status_code == 502:
sleep_time = min(base_delay * (2 ** attempt) + random.uniform(0, 1), 64)
print(f"Bad Gateway (502). Waiting {sleep_time:.2f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(sleep_time)
continue
response.raise_for_status()
return response
raise RuntimeError(f"Max retries ({max_retries}) exceeded for {url}")
Key design points:
- Always use the
Retry-Afterheader value when the server provides it — do not guess. - Add random jitter (0–0.5 s) to prevent thundering-herd when multiple workers retry simultaneously.
- Cap the maximum backoff at 64 seconds to avoid indefinite hangs.
- Raise after
max_retriesso callers know the operation failed rather than silently swallowing errors.
Step 3: Implement a Token Bucket Rate Limiter
For integrations making sustained high-volume requests, proactive rate limiting is more efficient than reactive retrying. A token bucket allows bursting up to the bucket capacity, then throttles to the refill rate.
import threading
import time
class TokenBucket:
def __init__(self, rate=3, capacity=10):
"""rate: tokens per second, capacity: burst limit"""
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self):
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return
# Not enough tokens — wait for next refill
wait_time = (1 - self.tokens) / self.rate
time.sleep(wait_time)
self._refill()
self.tokens -= 1
def _refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
new_tokens = elapsed * self.rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_refill = now
# Usage
bucket = TokenBucket(rate=2.5, capacity=5) # Stay just under the 3 req/s limit
def safe_notion_get(url, headers):
bucket.acquire()
return requests.get(url, headers=headers)
Setting the rate to 2.5 instead of 3.0 provides a safety margin against clock skew between your client and Notion's enforcement window.
Step 4: Use Notion's Batch-Friendly Endpoints
Many 429 errors are caused by unnecessary individual requests. Audit your code against these Notion API patterns that reduce call volume:
POST /v1/databases/{id}/query— Retrieve up to 100 pages in one paginated call instead of fetching pages individually.GET /v1/blocks/{id}/children— Retrieve all child blocks of a page in one call (paginated withstart_cursor).- Pagination — Always handle
has_more: truewithstart_cursorin a loop rather than making separate filtered queries.
Example of correct pagination to avoid excess requests:
def get_all_database_pages(database_id, headers):
pages = []
payload = {"page_size": 100}
while True:
bucket.acquire()
response = requests.post(
f"https://api.notion.com/v1/databases/{database_id}/query",
headers=headers,
json=payload
)
data = response.json()
pages.extend(data.get("results", []))
if not data.get("has_more"):
break
payload["start_cursor"] = data["next_cursor"]
return pages
Step 5: Persistent 502 Errors — When to Escalate
A single 502 is always safe to retry. However, if you observe:
- 502s sustained for more than 10 minutes
- 502s only on specific page or database IDs (may indicate corrupted content)
- 502s returned immediately without any delay (not a timeout)
Check status.notion.so for active incidents. If no incident is listed, file a support ticket with:
- The
x-request-idheader value from the failing response - The exact endpoint and method
- The timestamp (in UTC) of the failure
- Your integration ID (not the secret — the ID visible in integration settings)
Frequently Asked Questions
#!/usr/bin/env bash
# Notion API rate limit & 502 diagnostic script
# Usage: NOTION_TOKEN=secret_xxx PAGE_ID=xxx bash notion_diag.sh
NOTION_TOKEN="${NOTION_TOKEN:?Set NOTION_TOKEN env var}"
PAGE_ID="${PAGE_ID:?Set PAGE_ID env var}"
API_VERSION="2022-06-28"
BASE_URL="https://api.notion.com/v1"
echo "=== 1. Single request with full header inspection ==="
curl -si -X GET "${BASE_URL}/pages/${PAGE_ID}" \
-H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Notion-Version: ${API_VERSION}" \
| head -40
echo
echo "=== 2. Rapid-fire 5 requests — watch for 429 ==="
for i in $(seq 1 5); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${BASE_URL}/pages/${PAGE_ID}" \
-H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Notion-Version: ${API_VERSION}")
echo "Request ${i}: HTTP ${STATUS}"
done
echo
echo "=== 3. Check Retry-After header on 429 ==="
RETRY_AFTER=$(curl -si -X GET "${BASE_URL}/pages/${PAGE_ID}" \
-H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Notion-Version: ${API_VERSION}" \
| grep -i 'retry-after' | awk '{print $2}')
if [ -n "$RETRY_AFTER" ]; then
echo "Retry-After header: ${RETRY_AFTER} seconds"
else
echo "No Retry-After header present (not currently rate limited or 502)"
fi
echo
echo "=== 4. Extract x-request-id for 502 support tickets ==="
REQUEST_ID=$(curl -si -X GET "${BASE_URL}/pages/${PAGE_ID}" \
-H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Notion-Version: ${API_VERSION}" \
| grep -i 'x-request-id' | awk '{print $2}')
echo "x-request-id: ${REQUEST_ID:-not found}"
echo
echo "=== 5. Verify integration token is valid ==="
TOKEN_CHECK=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${BASE_URL}/users/me" \
-H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Notion-Version: ${API_VERSION}")
if [ "$TOKEN_CHECK" = "200" ]; then
echo "Integration token is VALID"
elif [ "$TOKEN_CHECK" = "401" ]; then
echo "ERROR: Token is INVALID or revoked (HTTP 401)"
elif [ "$TOKEN_CHECK" = "403" ]; then
echo "ERROR: Token lacks required permissions (HTTP 403)"
else
echo "Token check returned HTTP ${TOKEN_CHECK} — may be transient"
fiError Medic Editorial
The Error Medic Editorial team consists of senior DevOps engineers and SREs with experience operating large-scale API integrations across cloud and SaaS platforms. We specialize in translating cryptic HTTP errors into actionable runbooks, drawing on hands-on experience with rate limiting, retry strategy design, and production incident response.