Skip to content

Platform

Rate limits

Halyard limits how fast each project can call the API, so one runaway loop can't slow down everyone else. Limits are generous, visible in every response, and easy to handle.

On this page

Overview

Limits apply per project and per environment: your test keys and live keys have separate budgets, and all keys in a project share one. Most apps never see a 429. When they do, it is usually a backfill script or a retry loop without a delay.

  • Requests per second cover every API call, measured with a token bucket that allows short bursts.
  • Jobs and events per minute cap how much work you can enqueue, counting each job in a batch.
  • Worker connections cap how many long-polling workers can be connected at once.

Limits per plan

Rate limits per plan
PlanSustained req/sBurstJobs / minEvents / minWorkers
Hobby25501,50060010
Team10020010,0005,000100
Business5001,00060,00030,0001,000
EnterpriseCustomCustomCustomCustomCustom

Test keys get the Hobby limits on every plan. Worker polling calls don't count toward requests per second; they are covered by the worker connection limit instead.

Endpoint limits

A few endpoints have their own, lower limits on top of the plan limit:

Endpoint-specific limits
EndpointLimitWhy
POST /jobs/batch10 req/sEach request can carry up to 500 jobs.
GET /jobs25 req/sList queries scan more data; use webhooks instead of polling.
POST /queues/:id/purge1 req/minPurging is expensive and rarely needs repeating.
POST /keys10 req/hourProtects against leaked keys minting more keys.

Rate limit headers

Every response tells you where you stand, following the IETF RateLimit header fields. Read them to slow down before you are limited, not after.

Response headers
HTTP/2 201Content-Type: application/jsonRateLimit-Policy: 100;w=1, 200;burstRateLimit-Limit: 100RateLimit-Remaining: 87RateLimit-Reset: 1Halyard-Request-Id: req_4Tn8Wc2Ke7

Headers

RateLimit-Limitinteger
Requests allowed per second in the current window, for the limit closest to being reached.
RateLimit-Remaininginteger
Requests left in the current window. When it reaches 0, the next request is limited.
RateLimit-Resetinteger
Seconds until the window resets and RateLimit-Remaining refills.
RateLimit-Policystring
The policies that apply, like 100;w=1, 200;burst: 100 per one-second window, bursts up to 200.
Retry-Afterinteger
Only on 429 responses. Seconds to wait before retrying. Always honor it.

When you hit a limit

A limited request fails fast with 429 Too Many Requests and does nothing: no job is created, no event is published. The body is a standard error object with type rate_limited.

429 response
HTTP/2 429Content-Type: application/jsonRateLimit-Limit: 100RateLimit-Remaining: 0RateLimit-Reset: 2Retry-After: 2Halyard-Request-Id: req_8Lc3Vp6Yh1 {  "error": {    "type": "rate_limited",    "code": "requests_per_second",    "message": "Too many requests. Retry after 2 seconds.",    "request_id": "req_8Lc3Vp6Yh1",    "doc_url": "https://docs.halyard.example/docs/rate-limits"  }}

Retry with backoff

The official SDKs already retry 429 and 5xx responses, honoring Retry-After. If you call the API directly, wait for Retry-After when it is present and use exponential backoff with jitter when it isn't. Jitter spreads retries out so many clients don't come back at the same instant.

const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); export async function withBackoff(request, options = {}) {  const { maxAttempts = 5, baseMs = 500, capMs = 30_000 } = options;  for (let attempt = 1; ; attempt++) {    const res = await request();    if (res.status !== 429 && res.status < 500) return res;    if (attempt === maxAttempts) return res;     // Prefer the server's hint; otherwise exponential backoff with full jitter.    const retryAfter = Number(res.headers.get("Retry-After"));    const delay = retryAfter > 0      ? retryAfter * 1000      : Math.random() * Math.min(capMs, baseMs * 2 ** attempt);     await sleep(delay);  }}

Burst vs sustained

Each project has a bucket of tokens. Every request takes one; the bucket refills at the sustained rate and holds at most the burst size. A quiet project builds up a full bucket, so it can spend 200 requests in a moment on the Team plan, then continue at 100 per second.

Burst200bucket size
Sustained100/srefill rate
Cost1token per request
Allowed (top number) Limited with 429 (bottom number)A spike of 350 requests per second at 2s: the full bucket lets 200 through, then the project is held to 100 per second until traffic drops and the bucket refills.

In practice: short spikes, like a deploy that enqueues a few hundred jobs, are absorbed by the burst. Long imports need to stay under the sustained rate, or use batch create.

Send fewer requests

  • Batch your creates. POST /jobs/batch takes up to 500 jobs and counts as one request against requests per second.
  • Use webhooks, not polling. Subscribe to job.succeeded and job.failed rather than calling GET /jobs/:id in a loop.
  • Publish events for fan-out. One event can start jobs on many queues through subscriptions.
  • Need more? Business and Enterprise plans can raise limits. Write to support@halyard.example with your project ID and expected peak.
Create jobs in one request
curl https://api.halyard.example/v3/jobs/batch \  -H "Authorization: Bearer $HALYARD_KEY" \  -H "Content-Type: application/json" \  -d '{    "jobs": [      { "queue": "emails", "task": "send-digest", "payload": { "user_id": "usr_8KfQ2" } },      { "queue": "emails", "task": "send-digest", "payload": { "user_id": "usr_1Mx4P" } }    ]  }'

Suggested