import { GuideHero, GuideSection, GuideOut } from '~/components/Guide';
import { GuideArticleSchema } from '~/components/Guide/GuideArticleSchema';
import { CopyAsMarkdown } from '~/components/CopyAsMarkdown/CopyAsMarkdown';

export const metadata = {
  title: 'HubSpot API rate limits — what HS-X does about them',
  description: 'HubSpot API rate limits by tier, what the response headers mean, and how HS-X’s sync engine batches, backs off, and retries so you don’t have to.',
  alternates: { canonical: '/docs/rate-limits' },
  openGraph: {
    type: 'article',
    title: 'HubSpot API rate limits — what HS-X does about them',
    description: 'HubSpot API rate limits by tier, what the response headers mean, and how HS-X’s sync engine batches, backs off, and retries so you don’t have to.',
    url: '/docs/rate-limits',
  },
};

<GuideArticleSchema
  url="/docs/rate-limits"
  title="HubSpot API rate limits — what HS-X does about them"
  description="HubSpot API rate limits by tier, what the response headers mean, and how HS-X's sync engine batches, backs off, and retries so you don't have to."
  datePublished="2026-05-19"
  keywords={[
    'HubSpot API rate limits',
    'HubSpot 429',
    'x-hubspot-ratelimit-remaining',
    'HubSpot daily limit',
    'HubSpot batch API',
    'HS-X sync engine',
  ]}
  breadcrumb={[
    { name: 'Home', url: '/' },
    { name: 'Docs', url: '/docs' },
    { name: 'Reference', url: '/docs' },
    { name: 'Rate limits', url: '/docs/rate-limits' },
  ]}
/>

<CopyAsMarkdown src="/docs/rate-limits.md" />

<GuideHero
  eyebrow="Reference · Platform"
  title="HubSpot API rate limits."
  tagline="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."
  duration="≈ 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."
/>

<GuideSection eyebrow="TL;DR" title="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.

<GuideOut label="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.
</GuideOut>

</GuideSection>

<GuideSection eyebrow="01 · Matrix" title="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.

| Tier | Burst (per 10s) | Daily pool | Search API (per 10s) | Batch read (per 10s) |
| --- | --- | --- | --- | --- |
| **Free / Starter** | 100 | 250,000 | 4 | 5 |
| **Pro** | 150 | 500,000 | 5 | 6 |
| **Enterprise** | 190 | 1,000,000 | 7 | 8 |
| **Marketplace app (per install)** | 150 | 500,000 | 5 | 6 |
| **Marketplace · API Limit Increase** | 200 | 1,000,000 | 10 | 10 |

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.

</GuideSection>

<GuideSection eyebrow="02 · Headers" title="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.

| Header | Meaning |
| --- | --- |
| `X-HubSpot-RateLimit-Daily` | Your tier's daily ceiling. Constant per token. |
| `X-HubSpot-RateLimit-Daily-Remaining` | Calls left until midnight UTC. The one to alert on. |
| `X-HubSpot-RateLimit-Interval-Milliseconds` | Length of the burst window (always `10000`). |
| `X-HubSpot-RateLimit-Max` | Your tier's burst ceiling for this window. |
| `X-HubSpot-RateLimit-Remaining` | Calls left in this 10-second window. |
| `X-HubSpot-RateLimit-Secondly-Remaining` | Calls left in this *one-second* sub-window (Marketplace only). |

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

| Header | Meaning |
| --- | --- |
| `Retry-After` | Seconds to wait before retrying. Integer, usually 1–10 for burst exhaustion, hours for daily exhaustion. |

```sh
$ 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.

</GuideSection>

<GuideSection eyebrow="03 · What HS-X does" title="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.

<GuideOut label="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.
</GuideOut>

</GuideSection>

<GuideSection eyebrow="04 · Daily pool" title="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.

```sh
$ 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.

</GuideSection>

<GuideSection eyebrow="05 · Levers" title="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`](https://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.

</GuideSection>

<GuideSection eyebrow="06 · Debugging" title="What to do when you see a 429 anyway">

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

```sh
$ 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.

</GuideSection>

<GuideSection eyebrow="Related" title="Related reference">

- [Sync engine internals](/docs/syncs) — how cursors, batching, and retries fit together end-to-end.
- [Workflow actions and `hubspot.fetch`](/docs/workflow-actions) — when you bypass the engine and what to budget for.
- [Webhooks vs polling](/docs/webhooks) — replacing a high-frequency sync with a hook is the most common way to win back daily-pool headroom.
- [Answer · Which platform version should I be on?](/docs/answers/which-hubspot-platform-version) — orthogonal to limits, but the second question that comes up in a quota incident.

---

*Last updated: May 19, 2026. Limit figures sourced from HubSpot's [API usage guidelines](https://developers.hubspot.com/docs/api/usage-details). HS-X engine behavior reflects the runtime shipped in CLI `v3.1.0`. Refreshed whenever HubSpot adjusts a tier or the engine ships a behavioral change.*

</GuideSection>
