HatchedDocs
Reference

Rate limits

Per-principal plan-tier quotas, the 429 response shape, and how to back off gracefully.

Rate limits protect shared infrastructure. They're generous for normal product integrations but exist to prevent runaway loops.

Quotas

Rate limiting is per principal — one bucket per API key (hashed) — not per endpoint. Every request, regardless of route, draws from the same two sliding windows: a per-minute limit and a per-day limit. There are no per-second or per-endpoint quotas. /events, /eggs and /widget-sessions all consume the same bucket.

The size of that bucket comes from the customer's plan tier:

PlanPer minutePer day
Starter1005,000
Growth1,00050,000
Pro5,000250,000
Enterprise50,000unlimited

A few routes use different buckets:

BucketPer minutePer day
Widget & embed-token routes (/widget*, /embed-tokens)3× the plan limit3× the plan limit
Scoped principals (publishable key, widget/dashboard JWT)20010,000
Public share & profile pages602,000
Unauthenticated routes20200
Auth burst (login / register / password)10100

The windows are 60 seconds (per minute) and 86,400 seconds (per day). Higher plan tiers raise both the per-minute and per-day limits — talk to sales if you need a larger bucket.

The 429 response

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Please retry after 60 seconds.",
    "requestId": "…"
  }
}

The body has no details object. The wait value comes from the Retry-After response header — the full window in seconds (60 for the per-minute limit, 86400 for the per-day limit), not a small value. The SDK reads that header into RateLimitError.retryAfter (defaulting to 60s if absent).

Built-in retry

The SDK retries 429s automatically (honouring Retry-After) up to maxRetries (default 3) with exponential backoff + jitter. You only need manual backoff for sustained overages or custom queue drains.

new HatchedClient({
  apiKey: process.env.HATCHED_API_KEY!,
  maxRetries: 5, // default 3
});

Backoff pattern

import { RateLimitError } from '@hatched/sdk-js';

async function sendWithBackoff(event) {
  for (let attempt = 0; attempt < 5; attempt++) {
    try {
      return await hatched.events.send(event);
    } catch (err) {
      if (err instanceof RateLimitError) {
        await sleep((err.retryAfter + Math.random()) * 1000);
        continue;
      }
      throw err;
    }
  }
  throw new Error('rate limit exhausted');
}

Add jitter (the Math.random() term) so concurrent callers don't all retry on the same millisecond.

Bulk ingestion

For high-volume backfills, don't serialise through events.send. Use POST /events/batch (SDK: events.sendBatch) instead:

  1. Group up to 100 events per request (the batch cap).
  2. Each call processes synchronously and returns per-event results inline (see HTTP API).
  3. Accepted events are deduped by (customer_id, event_id); each non-duplicate event counts against your monthly event quota. The batch endpoint shares the same per-minute / per-day rate-limit bucket as every other write — there is no cap bypass and no separate async throughput quota. A 402 (event_quota_exceeded) is returned if the monthly event quota is exhausted.

Headers

Every response includes:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 98
X-RateLimit-Reset: 1745327400

X-RateLimit-Limit is your bucket's per-minute limit (here, 100 on the Starter plan). X-RateLimit-Reset is a Unix epoch-seconds timestamp marking the end of the current window. Use them to pace yourself before a 429 fires.