view .md
Reference · Platform

HubSpot API rate limits.

HubSpot rate-limits every API token along two axes — a per-second burst and a per-day pool — and a 429 from either one halts your sync. HS-X's sync engine batches against the burst, tracks the daily pool, and backs off on Retry-After so you almost never see a 429 in your own code. This page is what the engine is doing and how to read the headers when something does slip through.

Time
≈ 9 min read
Outcome
A working mental model of HubSpot's two-axis rate-limit system, what each response header means, and the four levers you can pull when you're close to a limit.

The 30-second answer

HubSpot enforces two limits on every API call: a burst limit (per-second) and a daily pool. Both are scoped to the access token, not the portal — so a private app and an OAuth app on the same portal don't share quota.

For most apps the practical ceilings are 150 requests per 10 seconds and 250,000 requests per day. Marketplace and enterprise tiers raise both. The full matrix is below.

HS-X's sync engine never issues a raw single-object write when a batch endpoint exists — every sync call routes through /batch/* (up to 100 records per call) and pauses on Retry-After. You'd have to do something unusual — a hand-written hubspot.fetch loop in a workflow action — to hit a 429 from inside an HS-X project.

If you only read one thing

Use batch endpoints; trust Retry-After; watch the daily pool, not the burst. The burst recovers in 10 seconds. Burning the daily pool means your sync stops shipping data until midnight UTC.

The limit matrix by tier

Every HubSpot account has a tier; every tier has a different ceiling. The numbers below are the public figures HubSpot publishes; private apps inherit the portal tier, public/marketplace apps get their own.

TierBurst (per 10s)Daily poolSearch API (per 10s)Batch read (per 10s)
Free / Starter100250,00045
Pro150500,00056
Enterprise1901,000,00078
Marketplace app (per install)150500,00056
Marketplace · API Limit Increase2001,000,0001010

A few notes that matter more than the numbers themselves:

  • Search and Batch have their own buckets. POST /crm/v3/objects/*/search and POST /crm/v3/objects/*/batch/* each count against a separate, much tighter per-10s bucket. A search-heavy workload runs out of search quota long before it runs out of general quota.
  • The daily pool resets at midnight UTC, not a rolling 24-hour window. A burst at 23:55 UTC followed by another at 00:05 UTC counts against two different days.
  • OAuth apps and private apps don't share. Each token has its own counters. If your sync uses a private app token and a workflow action uses an OAuth token, they have independent quotas.
  • The free tier of the Marketplace API Limit Increase is one credit per developer account. Beyond that it's a paid add-on; HubSpot rates the increase on a per-app basis.

What the response headers tell you

Every 2xx and every 429 from a HubSpot API call carries the same six headers. Read them once and you can debug any quota problem without leaving your terminal.

HeaderMeaning
X-HubSpot-RateLimit-DailyYour tier's daily ceiling. Constant per token.
X-HubSpot-RateLimit-Daily-RemainingCalls left until midnight UTC. The one to alert on.
X-HubSpot-RateLimit-Interval-MillisecondsLength of the burst window (always 10000).
X-HubSpot-RateLimit-MaxYour tier's burst ceiling for this window.
X-HubSpot-RateLimit-RemainingCalls left in this 10-second window.
X-HubSpot-RateLimit-Secondly-RemainingCalls left in this one-second sub-window (Marketplace only).

When you get a 429 Too Many Requests, HubSpot also returns:

HeaderMeaning
Retry-AfterSeconds to wait before retrying. Integer, usually 1–10 for burst exhaustion, hours for daily exhaustion.
$ curl -sI -H "Authorization: Bearer $TOKEN" \
    https://api.hubapi.com/crm/v3/objects/contacts?limit=1
HTTP/2 200
x-hubspot-ratelimit-daily: 500000
x-hubspot-ratelimit-daily-remaining: 487211
x-hubspot-ratelimit-interval-milliseconds: 10000
x-hubspot-ratelimit-max: 150
x-hubspot-ratelimit-remaining: 148

The two numbers that actually drive decisions are Daily-Remaining (capacity planning) and Retry-After on a 429 (operational behavior). Everything else is context.

What the sync engine does for you

You almost never need to think about any of the above when you're writing HS-X code. The sync engine sits between your pull function and HubSpot, and applies four behaviors automatically.

  • Batching. Every write goes through /crm/v3/objects/<type>/batch/{create,update,upsert,archive} in groups of 100. A 10,000-row sync issues 100 batch calls, not 10,000 single calls. This is the single biggest reason HS-X users don't see 429s.
  • Adaptive concurrency. The engine starts at 4 parallel batch calls and adjusts up or down based on X-HubSpot-RateLimit-Remaining. If you drop below 20% headroom it halves concurrency; if you stay above 70% for two windows it doubles back up.
  • Retry-After honoring. A 429 pauses the entire sync (not just the failing batch) for the duration the header asks for, then resumes from the same cursor. Sub-second retries use the literal value; daily-exhaustion retries (Retry-After > 60) park the sync until next midnight UTC and emit a daily.exhausted event.
  • Search budget guard. Calls to /search endpoints are tracked separately. The engine reserves at least one search slot per window for record-level lookups (the kind you need for reconciliation) so a bulk search-driven backfill can't starve the rest of the sync.
What you'd have to do to hit a 429

Write a sync pull that calls hubspot.fetch('/crm/v3/objects/contacts/{id}', ...) per row instead of returning records for the engine to batch. The engine can only batch what you hand it; per-record fetches inside pull are outside the budget.

Capacity planning against the daily pool

The burst limit is self-healing — 10 seconds and it's back. The daily pool is the one that bites. A back-of-envelope sizing:

  • A 5-minute sync of N records issues roughly ceil(N/100) batch writes plus 1–2 reads per run. At N = 10,000 that's 102 calls × 288 runs/day = 29,376 calls/day for one sync. Well inside any tier.
  • A daily full-table refresh of N records issues ceil(N/100) writes once. At N = 1,000,000 that's 10,000 calls/day. Also fine.
  • A search-driven reconciliation issues 1 search call per page (100 records). At 1M records that's 10,000 search calls/day — and the search bucket is much tighter, so this is where you start to plan around search.budget rather than the main pool.

The CLI's hs-x analyze quota command projects a 30-day quota burn from your current sync configurations against your portal's actual tier. Use it before turning on a new high-frequency sync; the output is usually enough to head off a quota incident a week before it happens.

$ hs-x analyze quota --window 30d
sync                       calls/day    daily share    p99 headroom
airtableContacts (5m)         29,376         5.9%           94%
stripeInvoices (1h)            2,304         0.5%           99%
salesforceDeals (15m)         12,672         2.5%           97%
                              ───────       ─────
total                         44,352         8.9%           91%

p99 headroom is the engine's projection of "in the worst case, how much of the daily pool is left when the sync finishes for the day." Anything below 25% should prompt a conversation about cutting frequency or upgrading tier.

The four levers when you're hitting a limit

When hs-x analyze quota flags you, or when you start seeing daily.exhausted events in the dashboard, there are four moves — in order of cost.

  1. Move from individual to batch. Already done if you only use worker.sync. If you have hand-written hubspot.fetch calls in workflow actions, replacing them with /batch/* cuts request count by ~100×. Cheapest and almost always available.
  2. Drop the frequency. A 5m sync that mostly returns "no new rows" is mostly waste. Switch to a webhook-driven hook if your source supports it, or back off to 15m. Most syncs don't need the freshness their schedule implies.
  3. Stretch the daily window with regional pinning. HubSpot's daily reset is UTC; if your team works US-Pacific, a heavy sync scheduled for 06:00 PT (= 13:00 UTC) leaves the rest of your day on a fresh pool. Set schedule: { at: '13:00', tz: 'UTC' } to pin.
  4. Request a Marketplace API Limit Increase. Available to listed marketplace apps and to enterprise portals on request. HubSpot reviews per-app and grants in 2× / 4× tiers. Submit via developers.hubspot.com/marketplace/listings/api-limits. Approval typically takes 5–10 business days; you'll need a description of the workload and a sample sync configuration.

What to do when you see a 429 anyway

The engine emits a structured event on every 429. From the CLI:

$ hs-x logs --filter rate_limit --tail
2026-05-19T14:22:08Z  sync=airtableContacts  endpoint=POST /batch/upsert
  status=429  retry_after=3s  daily_remaining=412,003  attempt=1/5
2026-05-19T14:22:11Z  sync=airtableContacts  endpoint=POST /batch/upsert
  status=200  daily_remaining=411,898  recovered_after=3.1s

Three things to check, in order, before assuming the engine is misbehaving:

  • Is the 429 from Retry-After: <small> or Retry-After: <hours>? Small means burst; the engine handled it and the sync continued. Hours means daily exhaustion; check hs-x analyze quota to see what burned the pool.
  • Was the 429 on /search or on a write endpoint? A /search 429 with the main pool still healthy means you have a search-heavy workload and need to either index data into HubSpot properties (so you can GET instead of /search) or add HS-X's search.budget: 'reserve' mode.
  • Was the token a private app or OAuth? A 429 only on the private app, with OAuth still healthy, points at a workflow-action or hook handler doing per-row fetches outside the sync engine.

If the engine is misbehaving — concurrency stays high through a sustained 429 burst, or Retry-After is ignored — file an issue with the contents of hs-x logs --filter rate_limit --window 1h --format json. The structured event log is enough to reproduce locally; no portal credentials are needed.