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.

bash
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:

json
{
  "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.

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:

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:

js
// 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 as event_type, or have neither user_id nor device_id.
  • events_with_invalid_fields: events with a value Amplitude can't accept, such as a time that isn't milliseconds since epoch, or a user_id shorter 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:

json
{
  "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:

  1. Collect every index from events_with_missing_fields, events_with_invalid_fields, and silenced_events.
  2. Log or store those events so you can fix them later.
  3. Resend the other events with their original insert_id values. 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

  1. Check that each response returns 200, and that events_ingested matches the number of events you sent. events_ingested counts events Amplitude accepted, including repeats it later ignores because of their insert_id.
  2. In Amplitude, open User Lookup and search for a user_id or device_id from the batch. Confirm the events appear on that user's stream.
  3. 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.

이 페이지에서