Table of contents

Congratulations: you finally pushed your integration to prod. Everything should go fine, right? After all, pulling only 5 candidates from Greenhouse’s API worked like a charm.
Then the trouble starts – 429 Error: Rate Limit Exceeded.
Feels like you got caught red-handed. You got rate limited. Turns out pulling 50,000 candidates didn’t scale as well as you’d thought.
More importantly: if it’s this easy to get rate limited for one integration, how do you maintain your code base for integrations at scale? At Merge, our team has faced, and overcome, this exact challenge. In this article, we'll cover how the Merge engineering team approached implementing a robust rate limit tracker.
In this article, we’ll share recommendations on how to implement a robust rate limit tracker into your platform and methods for avoiding rate limits when building integrations.
If you're interested in learning more about what rate limiting is, why they exist, and the types of rate limiting you'll run into -- we have a detailed article on this here.
The fundamental unit of tracking rate limits is a “data sync worker” that manages syncing data for a given customer. As a component of this work, you should track where you are in relation to a given API’s rate limits through a “Rate Limit Manager”. The “Rate Limit Manager” can track the rate limit configuration of a given API as well as details on your customers’ specific rate limit.
You’ll also need to track the overall API requests by your system in a shared memory cache and be able to orchestrate these data sync workers as part of a distributed system.
Rate limits can be difficult to manage across multiple systems for a variety of reasons.
Different APIs often have their own unique rate limiting policies. These policies can vary in terms of request limits per time frame (like per second, minute, or day), the type of rate limiting algorithm used (such as leaky bucket or token bucket), and how they handle limit breaches. Implementing a system that can adapt to and manage these varying policies efficiently is complex, as it requires a deep understanding and dynamic handling of each API's specific rate limiting mechanism.
When multiple APIs are integrated into a single system, especially in a distributed environment, ensuring that the rate limit tracking is accurate and up-to-date across all instances becomes a significant challenge. Synchronizing rate limit counters and managing concurrent requests across different servers or nodes to prevent rate limit breaches requires sophisticated coordination and real-time data sharing mechanisms, which can be difficult to implement effectively.
Some APIs adjust their rate limits dynamically based on factors like server load or user-specific criteria (e.g., different limits for free versus premium users). This dynamism adds another layer of complexity, as the rate limiting system must be able to detect, interpret, and adjust to these changes in real-time. It should also be capable of handling rate limits on a per-user basis, which requires a more granular level of control and monitoring.
The most basic function of a rate limit manager is the ability to programmatically define all the different rate limits you want to account for. It’s not uncommon for platforms to have more than one rate limit or even variations of different types. You’ll want to be able to define one or more rate limits per platform you’re integrating with.
We’ve also found that while the majority of rate limit tracking occurs at a platform-wide level, some APIs adjust rate limits for specific end-users. Therefore, our rate limit tracker stores two types of information: platform-specific details and end-user-specific details.
The definitions of platform-specific rate limit tracking we recommend using are:
Related: Everything you need to know about multiple API integration
Rate limit configurations are great for defining what a rate limit looks like for an entire 3rd party API, and for the majority of APIs that you deal with this definition alone should suffice. But after working with so many different APIs and seeing many, many different rate limits Merge noticed a trending design choice: platforms will change the details of a rate limit for an end-user’s specific use of that API.
Two factors that influence this are:
Differences in rate limits per customer are often defined in either the docs of an API, or they’re returned via API headers so that a user can dynamically determine the rate limit they must abide by. In these cases, Merge tracks these details per end-user using the following definitions as a template of default values that are overridden when we start fetching data.
Our End-User Rate Limit Definition has the following attributes:
Now let’s work through how you can best store information about how close you are to a given rate limit. You don’t want to call an API blind!
Regardless of whether you’re making API calls on a single server or a distributed system, the same general logic for choosing where to track a rate limit applies:
Which storage you use depends on the timeframe for the rate limit. For example:
The proper implementation of rate-limiting will utilize both methods. While performing syncs, it's likely best to use temporary storage while syncing along with periodic saves to persistent storage.
For robust applications, and to cover the 4 different types of rate limits, it is likely best to wrap all of your rate limit tracking into a manager. This manager can be accessed by your data engine and specifically called when your application:
Use this manager to implement your different storage access, and have a “check” method that will validate that you’re below thresholds before making an API call.


Related: How to manage API rate limits effectively
Because of the nature of rate-limiting, the solution is only to wait until we are below the threshold. However, we do have agency over how long we wait until we are below the threshold.
Merge uses two main approaches when we approach a rate limit:
The main factor in your decision is going to be the time frame of the limit. Sleeping a process is a waste of resources: if a rate limit is configured with a time period of several hours or a day, you’ll want to give those resources back to the machine and schedule a time later to try. But another consideration is that “starting from where I left off” will either not be possible for some 3rd Party APIs, or will be more computationally expensive than just sleeping for a few seconds and trying again.
When you’re making calls off of a simple application on your laptop or maybe even a single server, the two types of storage are pretty easy to work with:
For enterprise applications, you’ll likely have data syncs distributed amongst multiple servers or even entire clusters and data centers. Even for the same end-user, you could be running multiple syncs at once on different machines, but they all have to stay under one rate limit.
We want to keep the same 2-storage type approach, but change our technology implementation as follows:
On top of this, one final feature that we’ve built on top of our tracking system is Dynamic Default Thresholds.
We previously mentioned that rate limit thresholds are dynamic in Merge because they can be changed per customer and end-user enabling granular control over how much work Merge does on behalf of our customers. The thresholds can also be configured to be even more dynamic by being adjusted based on anticipated data load.
One consideration for why you should care about rate limiting is because you are likely not the only user of an API. While we can set a threshold and track the work that we’re doing, there’s not a good way to know where we stand against a total rate limit — not including the headers or API endpoints that tell you your current rate limit count.
To compensate for this, we set relatively conservative thresholds for our rate limits. Of course, this lower threshold is going to slow down data syncs because the more we run into the threshold, the more often we will either have to back off or try again later.
What would an ideal solution to this be? If we could know beforehand how much data we’d need to fetch, then we can specifically tailor a threshold to:
And? You guessed it — Merge has several techniques to estimate the amount of data we are about to start fetching and approximately how many requests it will take to do so. Before starting a sync we adjust the rate limit threshold (always respecting a customer-set limit) to either sync faster for higher data loads, or more conservatively for smaller ones.
Implementing this feature is difficult and likely the subject of a blog post in itself. After it's implemented, you just need to tune your rate limit management system for all of the different attributes listed above. Implementing such a system can be difficult, especially at scale, but is a vital part of any integration effort.
If you made it this far in the article then congrats! If you’re looking to implement such a system into your product, but either don’t have the time or resources to do so at scale, then consider Merge. We’ve already built out rate-limiting for our HRIS, payroll, ATS, CRM, ticketing, file storage, and Accounting systems, so schedule a demo.