Article
The Anatomy of a 429: How CRM Rate-Limit Algorithms Actually Work Under Load
You are pushing a heavy batch of lead updates into HubSpot or Salesforce. Your monitoring dashboard shows you have consumed maybe 5% of your daily API allowance. You have plenty of headroom.
Then, suddenly, the integration logs start screaming. Every request returns an HTTP 429. The pipeline stalls, the queue backs up, and your real-time sync is now fifteen minutes behind.
This is the classic GTM engineering trap. We focus on the high-level quotas—the daily limits sales reps use to justify an Enterprise license. But in high-throughput revenue systems, the daily quota is rarely why you fail. You are failing because of the internal geometry of the CRM's rate-limiting algorithms.
Platforms like HubSpot and Salesforce use a tiered defense system designed to protect their infrastructure from micro-bursts and long-running execution hangs. To build a reliable sync, you have to understand exactly which bucket you are starving.
The Three Layers of Throttling
Think of CRM limits as nested constraints. If any one of them is breached, the whole pipe shuts down.
1. The Daily Quota (Capacity): The rolling 24-hour window. This is for capacity planning and billing. If you hit this, you have usually under-provisioned your tier or you have a recursive loop in your code.
2. The Burst Limit (Throughput): Measured in seconds. HubSpot, for example, enforces a limit of 100 to 190 requests per 10-second window (depending on the plan). If you run a simple while loop that fires requests as fast as the CPU allows, you can exhaust a 10-second bucket in about 200 milliseconds.
3. The Concurrent Execution Limit (Saturation): This isn't about how many requests you send, but how long they take to finish. Salesforce allows a set number of long-running requests (those taking 20 seconds or longer) to happen simultaneously. If a complex Apex trigger or a massive database lock slows down your response time, you will hit a concurrency wall even if your total request volume is low.
Sliding Windows vs. Token Buckets
To manage bursts, platforms generally use one of two algorithmic patterns.
HubSpot uses a Sliding Window Log. Imagine a 10-second window that moves forward in real-time. The system tracks the timestamp of every request. When a new request arrives, it looks back exactly 10 seconds and counts the logs. If you are at request 101 and the first request happened 9.9 seconds ago, you are blocked. This creates a "spiky" failure pattern: your script works for a fraction of a second, fails for nine, then resumes.
Other systems use a Token Bucket. Think of a bucket that holds 100 tokens. Every API call costs one token. The bucket refills at a steady rate—say, 10 tokens per second. You can spend all 100 at once (a burst), but you are then limited to the refill rate of 10/sec until you stop and let the bucket replenish. This is generally more forgiving for GTM work because it allows for short bursts without immediate failure, provided your average speed stays within the refill rate.
Why Salesforce Concurrency is a Different Beast
Salesforce's concurrency limits (typically capped at 25 in production for long-running requests) are a common source of confusion. Most REST APIs care about throughput; Salesforce also cares about thread health.
This usually boils down to lock contention. If your automation tries to update the same Account record from multiple parallel threads, the database locks that record to ensure integrity. The second, third, and fourth requests sit in a queue waiting for the first to release the lock. This pushes their execution time over the 20-second threshold.
Suddenly, your entire integration is blocked with 429s or 503s. It isn't because you sent too much data; it's because your data is tripping over itself.
Decoding the Headers
Don't guess how much quota you have left. The CRM is telling you in every response header.
In HubSpot, watch for:
X-HubSpot-RateLimit-Daily-Remaining: Your long-term budget.X-HubSpot-RateLimit-Remaining: Your current burst window status.Retry-After: The exact number of seconds to wait.
If you ignore Retry-After and keep pounding the API, many platforms will penalize you by extending the lockout duration. Your code should inspect this header and pause the execution thread accordingly.
The Logic of Jitter and Backoff
A naive approach to a 429 is a "dumb retry." The script sees an error and immediately tries again. If you have ten parallel processes hitting a 429 simultaneously and they all retry at the exact same 1-second interval, you create a thundering herd. You hit the CRM with a massive spike the moment the window resets, triggering the 429 again.
The fix is exponential backoff with jitter. Instead of waiting 1, 2, 4, 8 seconds, you add randomness.
// Simplified Backoff with Jitter
const waitTime = (Math.pow(2, attempt) * baseDelay) + (Math.random() * 1000);
By adding a random variance (jitter), you spread the retry load across the timeline, allowing the rate-limit buckets to refill gracefully without being immediately slammed by a synchronized wave of requests.
When to Stop Using REST
If you spend your week tuning backoff algorithms, you might be using the wrong tool. The REST API is for synchronous, individual operations. It is not built for bulk data movement.
- HubSpot Batch Endpoints: You can update 100 contacts in a single call. This counts as one request against your 10-second burst limit. This reduces pressure by 99% immediately.
- Salesforce Bulk API 2.0: This is designed for millions of records. It bypasses the standard concurrency limits by uploading data to a temporary file, which Salesforce processes asynchronously when it has the capacity.
The Counterargument: Why Not Just Use an iPaaS?
You might argue that tools like Fivetran or Workato handle this for you. They do, but they are still bound by your CRM's limits. If your Salesforce instance is locked up because of a bad Apex trigger, even the most expensive integration platform will eventually lag or fail.
Understanding the underlying mechanics helps you realize that a 429 is often a symptom of an architectural problem—like record contention or poor batching—rather than a simple "too much traffic" problem. Sometimes the answer isn't a better retry loop; it's fixing the database lock that is making your requests take 20 seconds to finish in the first place.
— C.B.