How to Fix Notion API Rate Limit (429) and 502 Bad Gateway Errors
Resolve Notion API 429 Rate Limit and 502 Bad Gateway errors permanently using exponential backoff, request batching, and query optimization.
- Notion enforces a strict rate limit of 3 requests per second; exceeding this triggers a 429 Too Many Requests error with a Retry-After header.
- Notion API 502 Bad Gateway errors are often caused by upstream timeouts from unoptimized, deeply nested database queries rather than pure server outages.
- Quick Fix: Implement exponential backoff with jitter and respect the Retry-After header provided in the 429 response payload.
- Long-term Fix: Transition from synchronous API polling to an asynchronous queue system (like Redis/Celery) to control request concurrency.
| Method | When to Use | Implementation Time | Risk / Scalability |
|---|---|---|---|
| In-memory Retry Backoff | Small scripts, CLI tools, simple cron jobs | Low (15 mins) | High risk at scale (memory leaks, blocks thread) |
| Message Queue (Redis/RabbitMQ) | Production apps, webhooks, heavy sync operations | High (Hours/Days) | Low risk, highly scalable and durable |
| Response Caching | Read-heavy workloads with infrequent updates | Medium | Medium risk (Stale data if cache invalidation fails) |
| Query Pagination Optimization | Fixing frequent 502 Bad Gateway errors on DB queries | Medium | Low risk, dramatically improves API reliability |
Understanding the Notion API Rate Limit and 502 Errors
When building integrations with the Notion API, scaling your application will inevitably lead to encountering two distinct but deeply intertwined errors: the 429 Too Many Requests (Rate Limit) error and the 502 Bad Gateway error. While a 429 is an explicit rejection based on traffic volume, a 502 is often a symptom of backend stress caused by how you are requesting the data.
Notion enforces a strict rate limit across its API infrastructure. The documented limit is an average of 3 requests per second per integration. Bursting above this limit temporarily might be tolerated, but sustained polling or concurrent background jobs will quickly trigger a circuit breaker, returning an error object that looks like this:
{
"object": "error",
"status": 429,
"code": "rate_limited",
"message": "Rate limited. Please try again in 14 seconds."
}
Simultaneously, developers often experience HTTP 502 Bad Gateway errors. While traditionally a 502 implies an infrastructure issue between proxies (like Cloudflare and Notion's AWS instances), in the context of the Notion API, it frequently occurs when a single request takes too long to process. If you query a massive Notion database with complex relation rollups, filters, and no pagination limits, Notion's database workers may time out before returning the payload, resulting in a 502.
Step 1: Diagnosing the Root Cause
Before writing code, you must identify whether your integration is failing due to concurrency (429) or payload complexity (502).
Diagnosing 429 Errors:
Inspect your application's logs for HTTP response headers. Notion includes standard rate-limiting headers, but most importantly, it relies on the Retry-After header. If your application is firing multiple asynchronous requests simultaneously (e.g., using Promise.all in Node.js or asyncio.gather in Python to update 50 pages at once), you are guaranteeing a 429.
Diagnosing 502 Errors:
Look at the specific endpoints failing with 502s. Are they POST /v1/databases/{database_id}/query? If so, the issue is query complexity. Notion's backend struggles with deeply nested relations and rollups. If your query forces Notion to traverse multiple connected databases to resolve a filter, the upstream gateway will drop the connection after 10-15 seconds.
Step 2: Fixing 429 Rate Limits with Exponential Backoff
The fundamental fix for 429 Too Many Requests is to catch the exception, read the Retry-After header (or parse the message string if the header is stripped by an intermediary proxy), and pause execution. However, simply sleeping and retrying can cause the "thundering herd" problem if multiple threads wake up simultaneously.
Instead, implement Exponential Backoff with Jitter. This algorithm increases the wait time exponentially between consecutive failures and adds a random millisecond variance (jitter) to stagger retry attempts.
If you are using the official @notionhq/client for JavaScript or notion-client for Python, basic retries are built-in, but they often fail under heavy concurrent loads. You must wrap your API calls in a robust retry decorator or transition to a queuing system like Celery, BullMQ, or AWS SQS. By pushing Notion API calls to a queue, you can set a hard concurrency limit (e.g., max 2-3 workers) ensuring you never exceed the 3 requests/second threshold globally across your architecture.
Step 3: Resolving 502 Bad Gateway Errors via Query Optimization
Fixing 502s requires a different strategy. You cannot simply retry a 502 indefinitely; if the query is too heavy, it will fail 100% of the time. To resolve this:
- Reduce Page Size: The default
page_sizefor database queries is 100. Reduce this to25or50. While this increases the number of requests you must make (potentially risking a 429 if not throttled), it drastically reduces the backend processing time per request, virtually eliminating 502s. - Simplify Filters: Avoid filtering on Formula or Rollup properties if possible. Filtering on these property types forces Notion to calculate the values dynamically at query time for every row in the database. Instead, try to query the raw data and filter it in-memory within your application.
- Use ID-based Cursors: Always utilize the
start_cursorfor pagination. Never attempt to scrape a full database by incrementing offsets blindly.
Step 4: Architectural Best Practices for Notion Integrations
To build a truly resilient Notion integration:
- Implement a Local Cache: If you are building a Next.js or React application driven by a Notion backend (like a blog or documentation site), never query Notion on every page load. Use Static Site Generation (SSG) or implement a caching layer using Redis. Store the rendered Notion blocks and only invalidate the cache via Webhooks when a Notion page is updated.
- Decouple Writes: When users perform an action in your app that writes to Notion, acknowledge the user's action immediately, but place the Notion API write operation in a background queue. This prevents your UI from hanging if the Notion API is throttling requests.
- Monitor API Health: Rely on a centralized logging system (Datadog, Sentry, or ELK) to track the ratio of 200 OK responses to 429s and 502s. Set up alerts if the 429 rate exceeds 5% of your total traffic, indicating your backoff strategy needs tuning.
Frequently Asked Questions
import time
import random
import logging
from notion_client import Client
from notion_client.errors import APIResponseError
# Initialize Notion Client
notion = Client(auth="secret_your_notion_integration_token")
logging.basicConfig(level=logging.INFO)
def execute_with_backoff(api_call, max_retries=5):
"""
Executes a Notion API call with exponential backoff and jitter to handle
429 Rate Limits and transient 502 Bad Gateway errors.
"""
retries = 0
while retries < max_retries:
try:
return api_call()
except APIResponseError as e:
# Handle 429 Too Many Requests
if e.status == 429:
# Attempt to extract Retry-After from headers if available
# Fallback to parsing the error message or default backoff
retry_after = getattr(e, 'headers', {}).get('Retry-After')
if retry_after:
wait_time = float(retry_after)
logging.warning(f"Rate limited. Server requested wait of {wait_time}s.")
else:
# Exponential backoff: 2, 4, 8, 16, 32 seconds
base_wait = 2 ** retries
# Add jitter to prevent thundering herd
wait_time = base_wait + random.uniform(0, 1)
logging.warning(f"Rate limited. Backing off for {wait_time:.2f}s.")
time.sleep(wait_time)
retries += 1
# Handle 502 Bad Gateway (often due to heavy queries)
elif e.status == 502:
logging.error("502 Bad Gateway encountered. Query might be too heavy.")
# Give the server a moment to recover before retrying
wait_time = (2 ** retries) + random.uniform(1, 3)
time.sleep(wait_time)
retries += 1
else:
# Re-raise exceptions that are not 429 or 502 (e.g., 400 Bad Request, 401 Unauthorized)
raise e
except Exception as general_e:
# Handle network-level errors (e.g., connection reset)
logging.error(f"Network error: {general_e}")
time.sleep((2 ** retries) + random.uniform(0, 1))
retries += 1
raise Exception("Max retries exceeded while attempting Notion API call.")
# Example Usage: Safe Database Query with Pagination optimization
def safe_query_database(database_id):
all_results = []
next_cursor = None
has_more = True
while has_more:
def make_request():
return notion.databases.query(
**{
"database_id": database_id,
"start_cursor": next_cursor,
"page_size": 25 # Reduced page size to prevent 502s on large DBs
}
)
response = execute_with_backoff(make_request)
all_results.extend(response.get("results", []))
next_cursor = response.get("next_cursor")
has_more = response.get("has_more", False)
return all_resultsError Medic Editorial
The Error Medic Editorial team consists of senior SREs, DevOps engineers, and API architects dedicated to untangling the web's most frustrating infrastructure and integration errors. We specialize in robust, scalable solutions for modern REST APIs.