Error Medic

Notion API Rate Limit (HTTP 429 & 502): Complete Troubleshooting Guide

Fix Notion API rate limit errors (HTTP 429, 502 Bad Gateway) with exponential backoff, request queuing, and token rotation strategies. Step-by-step guide.

Last updated:
Last verified:
1,887 words
Key Takeaways
  • Notion enforces a hard limit of 3 requests per second per integration token; bursting above this triggers HTTP 429 with body {"object":"error","status":429,"code":"rate_limited","message":"You have been rate limited."}
  • HTTP 502 Bad Gateway from Notion typically indicates an upstream timeout or transient infrastructure fault — not a client error — and resolves with retry logic using exponential backoff
  • Quick fix: honour the Retry-After response header on 429s and implement exponential backoff with jitter (starting at 1 s, max 64 s) for both 429 and 502 responses; fan-out workloads across multiple integration tokens if throughput needs exceed 3 req/s
Fix Approaches Compared
MethodWhen to UseTime to ImplementRisk
Honour Retry-After + linear retryOccasional 429s in low-traffic integrations< 30 minLow — safe drop-in change
Exponential backoff with full jitterSustained burst traffic or batch imports1–2 hoursLow — industry-standard pattern
In-process request queue with token bucketHigh-throughput pipelines (> 2 req/s sustained)2–4 hoursMedium — requires queue infrastructure
Multiple integration tokens (fan-out)Need > 3 req/s to same workspaceHalf dayMedium — token management overhead
Notion SDK built-in retry (official)Node.js / TypeScript projects using @notionhq/client< 15 minVery low — supported by Notion
Circuit breaker patternProduction services where 502 storms must not cascade4–8 hoursLow after tuning — prevents thundering herd

Understanding Notion API Rate Limiting and 502 Errors

The Notion public API imposes a rate limit of 3 requests per second per integration token. This is a sliding-window limit applied server-side. When you exceed it, Notion returns:

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 wait before making another request."
}

HTTP 502 Bad Gateway errors are distinct — they originate from Notion's infrastructure layer (nginx/load balancer) timing out on an upstream worker. You will see:

HTTP/1.1 502 Bad Gateway
Content-Type: text/html

<html><head><title>502 Bad Gateway</title></head>...

or occasionally a JSON body when the API gateway itself catches the fault:

{
  "object": "error",
  "status": 502,
  "code": "internal_server_error",
  "message": "Internal server error."
}

Both errors are retryable. The key difference: 429 tells you to slow down; 502 tells you to wait and try again regardless of your request rate.


Step 1: Diagnose — Confirm Which Error You Are Hitting

Check the HTTP status code first. Do not rely solely on error messages — some HTTP clients wrap both 429 and 502 in a generic "request failed" exception.

With curl:

curl -sI -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer secret_YOURTOKEN" \
  -H "Notion-Version: 2022-06-28" \
  https://api.notion.com/v1/users/me

Expected output on rate limit: 429 Expected output on gateway fault: 502

Inspect response headers on 429:

curl -si \
  -H "Authorization: Bearer secret_YOURTOKEN" \
  -H "Notion-Version: 2022-06-28" \
  https://api.notion.com/v1/databases/YOUR_DB_ID/query

Look for Retry-After: N — N is the number of seconds to wait before retrying. Notion currently returns Retry-After: 1 for most rate limit responses, but treat this as dynamic.

Check your request rate — count outgoing requests in a rolling 1-second window. If you are using curl in a shell loop, add sleep 0.34 between calls (≈ 3 req/s). In Python, use time.sleep(). In Node.js, use setTimeout or a library like bottleneck.

Distinguish a 502 from infrastructure downtime:

# Check Notion status page programmatically
curl -s https://status.notion.so/api/v2/status.json | python3 -m json.tool

If status.indicator is "major_outage" or "partial_outage", pause your integration and subscribe to status updates rather than hammering retries.


Step 2: Fix — Implement Proper Retry Logic

Option A: Official Notion SDK (Recommended for Node.js / TypeScript)

The @notionhq/client SDK includes built-in retry logic. Enable it via the timeoutMs and retry options:

import { Client } from "@notionhq/client";

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
  timeoutMs: 60_000, // abort after 60 s total
  // Built-in retry: automatically retries 429 and 5xx up to 3 times
});

The SDK uses exponential backoff internally starting at 1 second. No additional configuration is required for standard use cases.

Option B: Python with Exponential Backoff and Jitter

For Python integrations, use the notion-client package or implement backoff manually:

import httpx
import time
import random
import os

NOTION_TOKEN = os.environ["NOTION_TOKEN"]
BASE_URL = "https://api.notion.com/v1"
HEADERS = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json",
}

def notion_request(method: str, path: str, **kwargs) -> dict:
    url = f"{BASE_URL}{path}"
    max_retries = 6
    base_delay = 1.0  # seconds

    for attempt in range(max_retries):
        response = httpx.request(method, url, headers=HEADERS, **kwargs)

        if response.status_code == 200:
            return response.json()

        if response.status_code == 429:
            retry_after = float(response.headers.get("Retry-After", base_delay))
            # Full jitter: sleep between 0 and retry_after * 2^attempt
            sleep_time = random.uniform(0, min(retry_after * (2 ** attempt), 64))
            print(f"Rate limited (429). Sleeping {sleep_time:.2f}s (attempt {attempt+1}/{max_retries})")
            time.sleep(sleep_time)
            continue

        if response.status_code in (502, 503, 504):
            sleep_time = random.uniform(0, min(base_delay * (2 ** attempt), 64))
            print(f"Gateway error ({response.status_code}). Sleeping {sleep_time:.2f}s")
            time.sleep(sleep_time)
            continue

        # Non-retryable error (400, 401, 403, 404)
        response.raise_for_status()

    raise RuntimeError(f"Notion API request failed after {max_retries} retries: {method} {path}")
Option C: Token Bucket Queue for High-Throughput Batch Jobs

For scripts that need to process thousands of Notion pages, enforce the rate limit on the client side to avoid hitting 429s at all:

import asyncio
import httpx
from asyncio import Semaphore

# Token bucket: max 3 concurrent requests with a 1-second refill window
REQUEST_SEMAPHORE = Semaphore(3)

async def bounded_notion_request(client: httpx.AsyncClient, method: str, path: str, **kwargs):
    async with REQUEST_SEMAPHORE:
        response = await client.request(method, f"https://api.notion.com/v1{path}", **kwargs)
        await asyncio.sleep(0.35)  # enforce ~2.8 req/s to stay under 3 req/s limit
        return response
Option D: Multiple Integration Tokens (Fan-Out)

If you legitimately need > 3 req/s throughput and have multiple Notion integrations configured, you can distribute requests across tokens. Each token has its own independent rate limit bucket:

import itertools

tokens = [
    os.environ["NOTION_TOKEN_1"],
    os.environ["NOTION_TOKEN_2"],
    os.environ["NOTION_TOKEN_3"],
]
token_cycle = itertools.cycle(tokens)

def get_headers():
    token = next(token_cycle)
    return {
        "Authorization": f"Bearer {token}",
        "Notion-Version": "2022-06-28",
    }

Note: Each token must be independently shared with the target workspace pages or databases. This approach is operationally complex — prefer client-side throttling first.


Step 3: Verify the Fix

After deploying retry logic, verify it works:

  1. Trigger a controlled 429 — send 10 requests in rapid succession and confirm your code logs backoff messages and eventually succeeds.
  2. Monitor retry metrics — log every retry attempt with the status code, attempt number, and sleep duration. Alert if attempt >= 4 (indicates sustained overload).
  3. Check Notion's x-ratelimit-* headers — Notion does not currently expose X-RateLimit-Remaining, but check future API versions for these headers.
  4. Load test with locust or k6 targeting a test database to confirm behaviour under sustained load before deploying to production.

Frequently Asked Questions

bash
#!/usr/bin/env bash
# notion-api-diag.sh — Diagnose Notion API rate limits and 502 errors
# Usage: NOTION_TOKEN=secret_xxx DATABASE_ID=xxx bash notion-api-diag.sh

set -euo pipefail

TOKEN="${NOTION_TOKEN:-}"
DB_ID="${DATABASE_ID:-}"
API_BASE="https://api.notion.com/v1"
NOTION_VERSION="2022-06-28"

if [[ -z "$TOKEN" ]]; then
  echo "ERROR: Set NOTION_TOKEN environment variable" >&2
  exit 1
fi

echo "=== 1. Check Notion status page ==="
curl -sf https://status.notion.so/api/v2/status.json \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('Status:', d['status']['indicator'], '-', d['status']['description'])"

echo ""
echo "=== 2. Verify token is valid (GET /users/me) ==="
HTTP_CODE=$(curl -sI -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Notion-Version: $NOTION_VERSION" \
  "$API_BASE/users/me")
echo "HTTP status: $HTTP_CODE"
if [[ "$HTTP_CODE" == "401" ]]; then
  echo "ERROR: Token invalid or revoked. Regenerate at https://www.notion.so/my-integrations"
  exit 1
fi

echo ""
echo "=== 3. Burst test — send 6 requests rapidly, observe 429 behaviour ==="
for i in $(seq 1 6); do
  START=$(date +%s%3N)
  CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Notion-Version: $NOTION_VERSION" \
    "$API_BASE/users/me")
  END=$(date +%s%3N)
  ELAPSED=$((END - START))
  echo "  Request $i: HTTP $CODE (${ELAPSED}ms)"
done

if [[ -n "$DB_ID" ]]; then
  echo ""
  echo "=== 4. Query database with retry-after inspection ==="
  curl -si \
    -X POST \
    -H "Authorization: Bearer $TOKEN" \
    -H "Notion-Version: $NOTION_VERSION" \
    -H "Content-Type: application/json" \
    -d '{"page_size": 1}' \
    "$API_BASE/databases/$DB_ID/query" \
    | grep -E '(HTTP/|retry-after|x-ratelimit|content-type|{)' \
    || true
fi

echo ""
echo "=== 5. Measure safe request interval ==="
echo "Safe rate: 3 req/s = min 334ms between requests"
echo "Recommended sleep between requests: 350ms (adds 5% buffer)"
echo "For async Python: asyncio.sleep(0.35)"
echo "For Node.js:      await new Promise(r => setTimeout(r, 350))"
echo "For shell loops:  sleep 0.35"

echo ""
echo "Diagnostics complete."
E

Error Medic Editorial

The Error Medic Editorial team consists of senior DevOps engineers, SREs, and API integration specialists with experience scaling integrations at high-growth SaaS companies. We write evidence-based troubleshooting guides grounded in production incident post-mortems and official vendor documentation.

Sources

Related Articles in Notion Api

Explore More API Errors Guides