Reference
Rate limits
How Emailit limits sending per workspace, the ratelimit headers on every send, what a 429 looks like, other endpoint limits and how to retry with backoff.
Emailit limits how fast and how much a workspace can send, not how many API calls you make. This page explains the sending limits, the headers that report them, the few endpoints with their own limits, and how to back off when you get a 429.
Sending limits
Every workspace has two sending limits. New workspaces start with these defaults on every plan:
| Limit | Default | Window |
|---|---|---|
| Per second | 2 emails | Sliding one-second window |
| Per day | 5,000 emails | Calendar day in UTC, resets at 00:00 UTC |
How the limits apply:
- Per workspace. All API keys and the SMTP relay share the same counters. Sending through SMTP uses up the same allowance as the API.
- Counted by recipients. Each unique address across
to,ccandbccis one email. A request with three recipients counts as three. - Only sends count. The limits apply to Send an email and Forward an email. Reading data and managing resources don’t count against them.
- Checked before, counted after. A send is accepted as long as the workspace hasn’t reached the limit yet, and its recipients are added to the counters afterward. One request with many recipients can take you past the limit, and the next requests get
429until the window clears.
You can see the current limits and today’s usage in the Sending Limits card on the Dashboard home page.
Rate limit headers
Responses from the send and forward endpoints include these headers:
| Header | Description |
|---|---|
ratelimit-limit |
Emails the workspace can send per second. |
ratelimit-remaining |
Emails left in the current one-second window. |
ratelimit-reset |
Seconds until the per-second window resets. |
ratelimit-daily-limit |
Emails the workspace can send per day. |
ratelimit-daily-remaining |
Emails left today. |
ratelimit-daily-reset |
Seconds until the daily limit resets at 00:00 UTC. |
retry-after |
Seconds to wait before retrying. Only sent with 429 responses. |
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
ratelimit-limit: 2
ratelimit-remaining: 1
ratelimit-reset: 1
ratelimit-daily-limit: 5000
ratelimit-daily-remaining: 4812
ratelimit-daily-reset: 52113When you hit a limit
A send over the limit returns 429 Too Many Requests with a retry-after header. The body says which limit you hit:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Maximum 2 messages per second allowed.",
"limit": 2,
"current": 2,
"retry_after": 1
}{
"error": "Daily limit exceeded",
"message": "Daily sending limit of 5000 messages has been reached.",
"limit": 5000,
"current": 5000,
"retry_after": 41760
}| Field | Description |
|---|---|
error |
Rate limit exceeded for the per-second limit, Daily limit exceeded for the daily limit. |
limit |
The limit that was reached. |
current |
How many emails were already counted in the window. |
retry_after |
Seconds to wait. 1 for the per-second limit, and the seconds until 00:00 UTC for the daily limit. |
Nothing is sent and no credits are used when a request is rejected. Retry per-second rejections after a short wait. For daily rejections, queue the email and send it after the reset, or ask for a higher limit.
Other limits
A few endpoints have limits of their own:
| Endpoint | Limit | Counted per |
|---|---|---|
POST /emails/{id}/forward |
3 per hour | Workspace |
POST /webhooks/{id}/test |
5 per minute | IP address |
Hosted subscribe and unsubscribe links (/subscribe/{token}, /unsubscribe/{token}) |
30 per minute | IP address |
Public form endpoints: load a form (GET /forms/{token}) |
60 per minute | IP address |
Public form endpoints: submit a form (POST /forms/{token}/submit) |
30 per minute | IP address |
Forwards also count against the sending limits above. Over the forward limit, the API returns 429 with a retry-after header:
{
"error": "too_many_requests",
"message": "Forwarding is limited to 3 emails per hour for this workspace. Try again later."
}These limits are fixed and don’t change with your plan or sending limits.
All other endpoints
Endpoints that read data or manage resources have no fixed per-endpoint limit. Keep your usage reasonable: use a small pool of concurrent requests rather than thousands in parallel, cache data that rarely changes, and use webhooks instead of polling for email status.
Raise your limits
- Pro and Business. Limits rise automatically as you send, based on your sending health. Emailit raises them at most once every seven days, and only while your sending health is good and none of your domains is paused.
- Every plan. Ask for an increase on the Dashboard home page: select Request Increase in the Sending Limits card, enter the per-second and per-day limits you need, where your list comes from and why. The support team usually reviews requests within 24 hours.
See Limits for every plan limit.
Retry with backoff
Retry 429, 409 (idempotency key in progress), 500 and 503 responses. Wait for retry-after seconds when the header is present, and use exponential backoff with jitter when it isn’t. Send an Idempotency-Key so a retried send is never delivered twice, and don’t wait out a daily limit in a request loop.
# curl retries 429 and 5xx responses and honors retry-after.
curl https://api.emailit.com/v2/emails \
--retry 5 --retry-max-time 60 \
-H "Authorization: Bearer $EMAILIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-confirmation" \
-d '{
"from": "Acme <orders@acme.com>",
"to": "ada@example.com",
"subject": "Your order #1042",
"text": "Thanks for your order."
}'import { randomUUID } from 'node:crypto';
const RETRYABLE = new Set([409, 429, 500, 503]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function sendEmail(payload, maxAttempts = 5) {
const idempotencyKey = randomUUID();
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await fetch('https://api.emailit.com/v2/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
const body = await response.json();
if (response.ok) return body;
const retryAfter = Number(response.headers.get('retry-after')) || 0;
// Daily limit: retry_after is hours away, so give up and queue it.
if (!RETRYABLE.has(response.status) || retryAfter > 60 || attempt === maxAttempts) {
throw new Error(`Emailit ${response.status}: ${body.message ?? body.error}`);
}
const backoff = Math.min(1000 * 2 ** (attempt - 1), 30_000);
await sleep(retryAfter ? retryAfter * 1000 : backoff + Math.random() * 250);
}
}import os
import random
import time
import uuid
import requests
RETRYABLE = {409, 429, 500, 503}
def send_email(payload, max_attempts=5):
idempotency_key = str(uuid.uuid4())
for attempt in range(1, max_attempts + 1):
response = requests.post(
"https://api.emailit.com/v2/emails",
headers={
"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json=payload,
timeout=30,
)
if response.ok:
return response.json()
retry_after = int(response.headers.get("retry-after", 0))
# Daily limit: retry_after is hours away, so give up and queue it.
if response.status_code not in RETRYABLE or retry_after > 60 or attempt == max_attempts:
response.raise_for_status()
backoff = min(2 ** (attempt - 1), 30) + random.random() / 4
time.sleep(retry_after or backoff)Tips for high volume
- Send from a queue with a fixed number of workers, and size the pool to your per-second limit.
- Spread large batch jobs over the day instead of starting them all at once, so a single job doesn’t use the whole daily allowance.
- Watch
ratelimit-daily-remainingand slow down before it reaches zero. - For newsletters to your audiences, use campaigns instead of looping over the send endpoint.