Error Medic

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.

Last updated:
Last verified:
1,820 words
Key Takeaways
  • 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
Fix Approaches Compared
MethodWhen to UseTime to ImplementRisk
Respect Retry-After headerFirst occurrence of 429, single-threaded client< 30 minLow
Exponential backoff with jitterRepeated 429/502 under sustained load1–2 hoursLow
Token bucket / request queueHigh-throughput integrations (>100 req/min)2–4 hoursMedium
Multiple integration tokensSeparate workspaces or parallel pipelines1 hourMedium — Notion ToS applies
Batch / bulk endpointsReading multiple pages/blocks in one call2–3 hoursLow
Client-side caching layerRepeatedly reading the same page data2–4 hoursLow
Webhooks instead of pollingEvent-driven pipelines that currently poll4–8 hoursLow — 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 limited
  • HTTP/2 502 → Notion-side transient failure
  • Retry-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-After header 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_retries so 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 with start_cursor).
  • Pagination — Always handle has_more: true with start_cursor in 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:

  1. The x-request-id header value from the failing response
  2. The exact endpoint and method
  3. The timestamp (in UTC) of the failure
  4. Your integration ID (not the secret — the ID visible in integration settings)

Frequently Asked Questions

bash
#!/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"
fi
E

Error 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.

Sources

Related Guides