Rate Limits
Himetrica applies rate limits at multiple layers to protect the service and ensure fair usage for all projects. This page covers every limit you may encounter.
Overview
Rate limits are enforced using a sliding window counter backed by Redis. There are three types of limits:
- Request rate limits — Per API key or per IP, applied to each endpoint
- Per-visitor limits — Prevents a single visitor from flooding custom events
- Plan limits — Monthly quotas for page views and custom events based on your subscription
All limits fail open — if the rate limiting infrastructure is temporarily unavailable, requests are allowed through rather than blocked.
Server API Limits
These limits apply per secret key (hm_sk_...) to the server-to-server API endpoints.
| Endpoint | Limit | Window |
|---|---|---|
POST /track | 1,000 requests | 1 minute |
POST /identify | 500 requests | 1 minute |
GET /user-id | 100 requests | 1 minute |
/revenue/* | 500 requests | 1 minute |
Client-Side Tracking Limits
The browser tracker scripts are rate-limited both per API key and per visitor IP address.
| Endpoint | Per API key | Per IP |
|---|---|---|
| Event tracking (global) | 10,000 / min | — |
| Page views | — | 10 / min |
| Custom events | — | 30 / min |
| Web Vitals | 1,000 / min | 20 / min |
| Error tracking | 1,000 / min | 20 / min |
| Beacon (unload) | — | 10 / min |
| Identify | — | 5 / min |
Per-Visitor Limits
In addition to the per-IP limits above, custom events have a per-visitor rate limit to prevent a single visitor from flooding your event quota:
| Limit | Details |
|---|---|
| 10 events / min | Per visitor per project. Events beyond this are silently dropped (the client receives a 202 to prevent retry loops). |
This limit protects against misbehaving scripts or bots that fire events in tight loops (e.g. ad impressions, scroll events). Legitimate visitor interactions rarely exceed 10 custom events per minute.
Silent drops
202 Accepted instead of 429 to prevent the client from retrying and amplifying the problem.Plan Limits
Each plan has monthly quotas for page views and custom events. These are tracked per organization and reset at the start of each billing cycle.
| Plan | Page views / mo | Custom events / mo |
|---|---|---|
| Free | 30,000 | 150,000 |
| Starter | 200,000 | 1,000,000 |
| Pro | 500,000 | 2,500,000 |
| Business | 2,000,000 | 10,000,000 |
| Scale | 10,000,000 | 50,000,000 |
A small grace period is applied before hard-blocking requests. You'll receive an email notification when you reach 100% of your quota. Requests are blocked at 115% of quota (200% for Business and Scale plans).
Response Headers
When a rate limit is exceeded, the API returns 429 Too Many Requests with the following headers:
| Header | Description |
|---|---|
Retry-After | Seconds to wait before retrying |
X-RateLimit-Limit | Your request limit for the current window |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Project Suspension
If a project accumulates more than 50 dropped events within a 5-minute window (from per-visitor rate limiting), all custom events for that project are temporarily suspended.
| Suspension | Duration |
|---|---|
| First suspension | 5 minutes |
| Repeat suspension (within 1 hour) | 15 minutes |
During suspension, all custom event requests return 202 without being processed. Page views, vitals, and error tracking are not affected. After the suspension expires with no further abuse, the counter resets.
Common causes
Handling 429 Responses
When using the Server API, implement exponential backoff with the Retry-After header:
async function trackWithBackoff(event, retries = 3) {
for (let i = 0; i < retries; i++) {
const res = await fetch("https://app.himetrica.com/api/v1/track", {
method: "POST",
headers: {
"X-API-Key": process.env.HIMETRICA_SECRET_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(event),
});
if (res.status !== 429) return res.json();
const retryAfter = res.headers.get("Retry-After");
const waitMs = (retryAfter ? parseInt(retryAfter) : Math.pow(2, i)) * 1000;
await new Promise((r) => setTimeout(r, waitMs));
}
throw new Error("Rate limited after retries");
}Batch when possible