Batch and retry HTTP V2 API requests
To send server-side events reliably, post up to 2,000 events in each HTTP V2 API request and give every event an insert_id. Retry 429 and 5xx responses with backoff. When a 400 response lists rejected events, remove those events and resend the rest.Amplitude ignores an event that repeats an insert_id from the past 7 days, so a retry doesn't create duplicates. For the exact rule, go to Event deduplication.
이 페이지는 아직 귀하의 언어로 번역되지 않았습니다. 현재 작업 중이므로 곧 다시 확인해 주십시오.
This guide covers the HTTP V2 endpoint, /2/httpapi. To send historical data, or more than 30 events per second for one user or device, use the Batch Event Upload API instead. That API accepts up to 1,000 events per second for each user or device, and requests up to 20 MB.
Prerequisites
- The API key for your Amplitude project.
- For the Node.js sample, Node.js 18 or later.
Send a batch request
Send a POST request with a JSON body that contains your api_key and an events array. Each event needs an event_type and either a user_id or a device_id. Add an insert_id that stays the same for the event across every retry.
curl -X POST https://api2.amplitude.com/2/httpapi \
-H 'Content-Type: application/json' \
-d '{
"api_key": "YOUR_API_KEY",
"events": [
{
"user_id": "user-20391",
"event_type": "Order Completed",
"time": 1793817970000,
"insert_id": "order-88231-completed"
},
{
"user_id": "user-20391",
"event_type": "Receipt Emailed",
"time": 1793817971000,
"insert_id": "order-88231-receipt"
}
]
}'
If your project uses Amplitude's EU data center, send requests to https://api.eu.amplitude.com/2/httpapi.
A successful response returns 200 and the number of events Amplitude accepted:
{
"code": 200,
"server_upload_time": 1793817970568,
"payload_size_bytes": 239,
"events_ingested": 2
}
To reduce bandwidth, gzip the request body and add a Content-Encoding: gzip header. Amplitude counts events against the 2,000-event limit after it decompresses the body.
Size your batches
Keep each request within these limits. The HTTP V2 upload limits are the canonical source.
| Limit | Value | Response when exceeded |
|---|---|---|
| Events per request | 2,000 | 413 |
| Request body size | Under 1 MB | 413 |
| Events per user or device | 30 per second, averaged over a rolling 30-second window | 429 |
| Events per project | 50,000 per second across the HTTP API and HTTP V2 endpoints | 429 |
On the Free plan, send no more than 10 events in each request, 100 requests per second, and 1,000 events per second.
At high volume, partition work by user_id or device_id, so that throttling for one user or device doesn't delay events for everyone else.
Retry failed requests
Decide whether to retry from the response status:
| Status | Meaning | What to do |
|---|---|---|
200 | Amplitude accepted the events. | Nothing. |
400 | The request or some events are invalid. | Don't retry unchanged. Go to Handle partial failures. |
403 | Amplitude's web application firewall blocked the request. | Don't retry. Check the request for values that security filters could match. |
413 | The request has more than 2,000 events or is too large. | Split the events into smaller requests, then send each one. |
429 | A user, a device, or the project exceeded its rate limit. | Wait 30 seconds, then retry. If the project exceeded its limit, wait 60 seconds. |
500, 502, 504 | Amplitude hit an error and may have accepted the events. | Retry with backoff. The insert_id values prevent duplicates. |
503 | Amplitude didn't accept the events. | Retry with backoff. |
| Network error or timeout | You don't know whether Amplitude accepted the events. | Retry with backoff. The insert_id values prevent duplicates. |
For 5xx responses and network errors, use exponential backoff with jitter. Wait a random time up to 1 second, then up to 2 seconds, then 4 seconds, up to a cap. Jitter keeps many workers from retrying at the same moment. Set a maximum number of attempts, and log or store any events you can't send.
This Node.js function sends a batch and applies every rule in the status table:
// Node.js 18 or later
const ENDPOINT = "https://api2.amplitude.com/2/httpapi"; // EU: https://api.eu.amplitude.com/2/httpapi
const API_KEY = process.env.AMPLITUDE_API_KEY;
const MAX_ATTEMPTS = 6;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Exponential backoff with full jitter: a random wait up to 1 s, 2 s, 4 s, ... capped at 60 s.
const backoff = (attempt) =>
Math.random() * Math.min(60_000, 1_000 * 2 ** attempt);
// Indexes of the events a 400 response rejected.
function rejectedIndexes(body) {
const indexes = new Set(body.silenced_events ?? []);
for (const fields of [
body.events_with_missing_fields,
body.events_with_invalid_fields,
]) {
for (const list of Object.values(fields ?? {}))
list.forEach((i) => indexes.add(i));
}
return indexes;
}
export async function sendEvents(events, attempt = 0) {
let res;
try {
res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: API_KEY, events }),
});
} catch (err) {
// Network error or timeout: Amplitude may or may not have the events, so retry.
if (attempt + 1 >= MAX_ATTEMPTS) throw err;
await sleep(backoff(attempt));
return sendEvents(events, attempt + 1);
}
const body = await res.json().catch(() => ({}));
if (res.ok) return body; // body.events_ingested
if (res.status === 413 && events.length > 1) {
// Too many events: split the batch in half and send each half.
const mid = Math.ceil(events.length / 2);
const first = await sendEvents(events.slice(0, mid));
const second = await sendEvents(events.slice(mid));
return { events_ingested: first.events_ingested + second.events_ingested };
}
if (res.status === 400) {
const rejected = rejectedIndexes(body);
if (rejected.size === 0) throw new Error(`Request rejected: ${body.error}`); // For example, an invalid API key.
console.error(
"Amplitude rejected these events:",
[...rejected].map((i) => events[i]),
body,
);
const rest = events.filter((_, i) => !rejected.has(i));
return rest.length ? sendEvents(rest, attempt) : { events_ingested: 0 };
}
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt + 1 >= MAX_ATTEMPTS) {
throw new Error(
`Upload failed with ${res.status}: ${JSON.stringify(body)}`,
);
}
// 429: wait 30 s, then double the wait on each repeat. 5xx: back off with jitter.
await sleep(
res.status === 429
? Math.min(300_000, 30_000 * 2 ** attempt)
: backoff(attempt),
);
return sendEvents(events, attempt + 1);
}
Handle partial failures
A 400 response can name the specific events that failed, by their index in the events array. These fields map each field name to the indexes of every event with that problem:
events_with_missing_fields: events that lack a required field, such asevent_type, or have neitheruser_idnordevice_id.events_with_invalid_fields: events with a value Amplitude can't accept, such as atimethat isn't milliseconds since epoch, or auser_idshorter than 5 characters.
A response can include both fields. For example, this response reports a missing event_type at index 0 and an invalid time at index 1:
{
"code": 400,
"error": "Request missing required field",
"events_with_missing_fields": { "event_type": [0] },
"events_with_invalid_fields": { "time": [1] }
}
A 400 for silenced devices lists the affected events in silenced_events.
To recover:
- Collect every index from
events_with_missing_fields,events_with_invalid_fields, andsilenced_events. - Log or store those events so you can fix them later.
- Resend the other events with their original
insert_idvalues. If Amplitude already stored any of them, it ignores the repeats.
If a 400 response lists no event indexes, the whole request is invalid. Check the error value, such as Invalid API key or Invalid JSON request body.
A 429 response lists the events from throttled users or devices in throttled_events. To keep other events flowing, you can send the remaining events right away and hold only the throttled ones for 30 seconds. Resending the whole batch after the wait is also safe, because of the insert_id values.
For the full response schemas, go to the HTTP V2 API response reference.
Verify your events
- Check that each response returns
200, and thatevents_ingestedmatches the number of events you sent.events_ingestedcounts events Amplitude accepted, including repeats it later ignores because of theirinsert_id. - In Amplitude, open User Lookup and search for a
user_idordevice_idfrom the batch. Confirm the events appear on that user's stream. - To review accepted and rejected requests across the project, use the Ingestion Debugger.
이 내용이 도움이 되었나요?
For AI agents: a documentation index is available at /docs/llms.txt. Append .md to any page URL for markdown, or send Accept: text/markdown.