Here is a scenario that happens more than people admit.
You build an automation. You test it. It works perfectly. You hand it off, everyone is happy, and it runs smoothly for weeks. Then one morning something breaks. Not loudly, not with an obvious error. Just silently. Records stop syncing. Notion pages are not updating. Data is drifting between systems and nobody noticed until a client called.
You dig in. The workflow logs show a bunch of failed requests. The error code is 429.
That is a rate limit. And if you did not design for it upfront, fixing it later is a pain.
What a rate limit actually is
Every API has one. Stripe, Notion, Attio, HubSpot, Slack, doesn't matter. They all put a cap on how many requests you can send in a given time window.
The reason is simple. APIs run on servers. Servers have finite resources. If every developer could hammer them with unlimited requests, the whole thing would fall over for everyone. Rate limits are how API providers protect themselves and make sure no single integration can monopolize the infrastructure.
The limit itself varies by provider. Some count requests per second. Some per minute. Some per day. Some have different limits for read operations versus write operations.
When you exceed the limit, the API stops processing your requests and sends back a 429 response. Too Many Requests. That request failed. Whatever your automation was trying to do, it did not happen.
And here is the part that catches people. By default, most automation tools just log the failure and move on. So your data does not sync. Your record does not update. Your trigger does not fire. And unless you built proper error handling, you might not know for hours.
The actual numbers for the APIs you probably use
This is not something most blog posts include but it matters. You need to know exactly what you are working with before you build something.
Notion API: 3 requests per second
Three. Per second. Per integration.
Notion allows some burst above this average, meaning you can briefly spike higher before getting throttled. But the sustained average is 3/s. In practical terms that is about 180 requests per minute, or roughly 10,800 per hour.
That sounds fine until you are syncing a CRM with 500 contacts. Or processing a webhook that needs to create a page, update five properties, and link three relations. Suddenly you are burning through 4 to 6 requests per record and the math stops working.
Notion also has a payload size limit: 1,000 block elements per request and a 500KB total body size. Hit either of those and you get a different kind of error, but it is worth knowing both limits exist.
Stripe API: 25 to 100 requests per second
Stripe is more generous. The default limit for most endpoints is 25 requests per second. Read operations like listing charges or fetching customer data go up to 100/s. The Search API is capped at 20/s. Payout creation is at 15/s with a maximum of 30 concurrent requests.
So Stripe is less likely to be your bottleneck in normal operation. But if you are building something that processes bulk payment data, like syncing historical invoice records or doing a large reconciliation run, you can hit those limits faster than you expect.
Attio API: 100 reads, 25 writes per second
Attio is actually quite generous on reads. 100 read requests per second gives you a lot of headroom. But writes are capped at 25/s, which is the same as Notion's total limit.
Attio also has something called score-based rate limiting on their more complex query endpoints. When you run a query with lots of filters and sorts on a large dataset, it gets assigned a complexity score. If that score is too high, you get rate limited regardless of how many requests per second you are sending. The window for this is 10 seconds. It is well documented in their API docs, but it trips people up because the 429 they see on a complex list query looks identical to a regular rate limit error.
Why it hits harder than you think
The math always looks fine in isolation. 3 requests per second is 180 per minute. Your integration does not send 180 requests per minute. So you should be fine, right?
Usually yes. Until you are not.
The problem is spikes. Real automations are not evenly distributed. They cluster.
Someone adds 50 new contacts to a CRM in one go. A webhook fires for all 50 simultaneously. Each contact needs a Notion page created, properties set, and relations linked. That is 150 to 200 Notion API requests arriving in the same second. You have a limit of 3.
Or a client clicks a button that triggers a workflow which queries three databases, creates two pages, updates four relations, and sends a Slack message. That single click just fired 10 API calls in less than a second.
The automation worked fine in testing because you were only testing one contact at a time, one click at a time. Production does not work that way.
The burst concept
Most APIs allow short bursts above the stated limit. Notion says their 3/s limit is an average. Stripe uses something called a token bucket algorithm where you accumulate allowance over time and can spend it in bursts.
What this means in practice is that you will not get rate limited the exact moment you send your 4th request in a second. The system tolerates brief spikes. But sustained traffic above the limit will get throttled.
Do not design your system to rely on burst tolerance. It is a buffer, not a feature. Treat the stated limit as the real limit and build for it.
How to actually handle rate limits
There are three things that work. You probably need all three for anything serious.
1. Exponential backoff with jitter
When you get a 429, wait and retry. But not with a fixed wait time. Fixed retries cause what is called a thundering herd problem. All your requests waited the same amount of time, so they all retry simultaneously, and you hit the rate limit again immediately.
Exponential backoff means you double the wait time on each retry. Jitter means you add a small random amount on top so all your retries don't land at exactly the same moment.
Here is the basic pattern:
async function requestWithBackoff(fn, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error.status === 429 && attempt < maxRetries - 1) {
// Check if the API gave us a Retry-After header
const retryAfter = error.headers?.['retry-after'];
const waitTime = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Rate limited. Waiting ${waitTime}ms before retry ${attempt + 1}`);
await new Promise(resolve => setTimeout(resolve, waitTime));
} else {
throw error;
}
}
}
}Always check for the Retry-After header first. Notion sends one. Attio sends one. Stripe sends one. It tells you exactly how long to wait. Using it is more reliable than calculating your own backoff.
2. Queues
For bulk operations, the right answer is a queue. Instead of firing all 50 requests simultaneously, you put them all in a queue and process them at a controlled rate.
In n8n you can do this with the batch node or by adding a Wait node between iterations. Not elegant but it works. For proper queue systems you would use something like BullMQ or even just a simple setTimeout-based throttle.
The idea is simple. You know your limit is 3 requests per second. So you process one request every 350ms and you never get close to the limit. Slower but bulletproof.
3. Batching
Some APIs let you combine multiple operations into a single request. Notion does not have a batch API for creating pages, but it does let you set multiple properties in a single update request. Attio has batch endpoints for certain operations.
Every time you can replace 5 requests with 1 request, you buy yourself room to breathe under the rate limit. Always look for batching options before assuming you need to make individual calls for everything.
The silent failure problem
The thing that makes rate limits genuinely dangerous is that failures are often silent.
A 429 is not a crash. Your automation keeps running. It just skips the request that got rate limited. If you are not explicitly handling 429 responses and either retrying or logging them somewhere visible, you will not know the data did not sync. The automation looks healthy in the dashboard. The error is in the data.
I have seen this in production more than once. A sync that worked for weeks started silently dropping records during a busy period when more data was flowing through. Nobody noticed until the Notion database was visibly inconsistent with the source system.
The fix for this is boring but important. Log every 429 response. Alert on them if the frequency gets above a threshold. Store failed requests somewhere so they can be retried rather than dropped.
What this looks like in practice
When I build CRM to Notion syncs, rate limits are not an afterthought. They are part of the design from day one.
For anything that needs to sync bulk records, the flow uses a queue. For real-time webhook-triggered updates, exponential backoff handles the occasional spike. And every failed request gets logged with enough context to know exactly which record failed and why, so nothing gets silently lost.
That is what separates a sync that holds up under real usage from one that looks fine until there is actual data volume flowing through it.
If your automation is running at low volume and you have never hit a 429, that is great. But it is worth building the retry logic before you need it rather than after a client notices records are missing.