---
title: Instrument and configure the Node.js SDK
description: "Track events, user and group properties, and revenue with the Amplitude Node.js SDK, then configure identity, plugins, batching, retries, regions, proxies, and serverless flushing."
product: analytics
lang: en
last_updated: 2026-11-10
token_estimate: 6927
---
# Instrument and configure the Node.js SDK

After your first event with the Node.js SDK (`@amplitude/analytics-node`), use these tasks to track events and properties, identify users, change events with plugins, tune how the SDK uploads, and flush events from short-lived processes. To install the SDK, go to [Get started](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/get-started). For every signature and option, go to the [Node.js SDK reference](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference). For changes between versions, go to the [Node.js SDK release notes](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk-release-notes).

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

The examples import named functions, which act on a default instance. Each instance from [`createInstance()`](#send-events-to-more-than-one-project) has the same methods.

## Track events

Amplitude's HTTP V2 API receives the events, so every event needs an `event_type` and a `user_id` or `device_id`. For field rules, go to the [HTTP V2 API](https://amplitude.com/docs/apis/analytics/http-v2).

### Track a custom event

Call [`track()`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#track) with an event name, optional event properties, and the event options that identify the user:

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

track("Button Clicked", { buttonColor: "primary" }, { user_id: "user-12345" });
```

#### Pass the event as an object

Pass an object to set any event field directly, such as `groups` or `user_properties`:

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

track({
  event_type: "Report Exported",
  event_properties: { format: "csv" },
  groups: { orgId: "15" },
  user_id: "user-12345",
});
```

If you also pass event properties as the second argument, they replace the object's `event_properties`. The two sets don't merge.

#### Set event fields for one call

The third argument takes any event field, such as `device_id`, `time`, `insert_id`, `ip`, `country`, or `app_version`. For the full list, go to [EventOptions](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#eventoptions).

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

track(
  "Order Shipped",
  { orderId: "ord-1001" },
  {
    user_id: "user-12345",
    device_id: "device-abcdef",
    time: Date.now() - 60_000, // when the event happened, in milliseconds
    insert_id: "order-shipped-ord-1001", // Amplitude deduplicates on this
    ip: "203.0.113.7",
  },
);
```

The SDK sets `time` and `insert_id` when you don't. It always overwrites `event_id` and `library`.

### Set user properties

Build an [`Identify`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#identify-class) object, chain operations on it, and send it with [`identify()`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#identify). The SDK sends an `$identify` event that updates the user's properties without counting as an action.

```js
import { init, identify, Identify } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

const props = new Identify()
  .set("plan", "pro")
  .setOnce("initial_plan", "starter")
  .add("login_count", 1)
  .append("visited_regions", "us-west")
  .unset("trial_ends_at");

identify(props, { user_id: "user-12345" });
```

```json
{
  "event_type": "$identify",
  "user_id": "user-12345",
  "user_properties": {
    "$set": { "plan": "pro" },
    "$setOnce": { "initial_plan": "starter" },
    "$add": { "login_count": 1 },
    "$append": { "visited_regions": "us-west" },
    "$unset": { "trial_ends_at": "-" }
  }
}
```

| Operation | Effect |
| --- | --- |
| `set(property, value)` | Sets the value. |
| `setOnce(property, value)` | Sets the value only if the property has no value yet. |
| `add(property, number)` | Adds the number to the current value. A property with no value starts at `0`. |
| `append(property, value)` | Adds the value or values to the end of the list. A property with no value starts as an empty list. |
| `prepend(property, value)` | Adds the value or values to the start of the list. A property with no value starts as an empty list. |
| `postInsert(property, value)` | Adds the value to the end of the list if the list doesn't contain it yet. |
| `preInsert(property, value)` | Adds the value to the start of the list if the list doesn't contain it yet. |
| `remove(property, value)` | Removes the value from the list, if it's there. |
| `unset(property)` | Removes the property. |
| `clearAll()` | Removes all of the user's properties. |

Each property takes one operation per `Identify` object. The SDK silently skips a second operation on the same property, so `new Identify().set("plan", "pro").setOnce("plan", "free")` sends only the `set`. After `clearAll()`, the SDK skips every other operation on that object. The SDK drops values that aren't valid properties, such as `null`, `undefined`, functions, and nested arrays, without a log message.

User property changes apply to events that arrive after the `$identify` event. The user's profile shows the change right away, but charts reflect it only on later events. For more, go to [User properties and events](https://amplitude.com/docs/data/user-properties-and-events).

### Assign a user to a group

Call [`setGroup()`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#setgroup) with a group type and one or more group names. For example, a user in organization `15`:

```js
import { init, setGroup } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

setGroup("orgId", "15", { user_id: "user-12345" });

// A user can belong to more than one group of a type.
setGroup("sport", ["tennis", "soccer"], { user_id: "user-12345" });
```

`setGroup()` sends an `$identify` event that sets the group type as a user property, such as `orgId: "15"`, and replaces any earlier value of that property. In charts, you can then count distinct groups, such as organizations, that performed an event.

To associate one event with a group without changing the user's groups, set `groups` on that event. Go to [Pass the event as an object](#pass-the-event-as-an-object).

### Set group properties

Call [`groupIdentify()`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#groupidentify) with a group type, a group name, and an `Identify` object. The changes apply to the group, not the user, and affect only later events.

```js
import { init, groupIdentify, Identify } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

const groupProps = new Identify().set("plan", "enterprise").set("seats", 250);

groupIdentify("orgId", "15", groupProps, { user_id: "user-12345" });
```

### Track revenue

Build a [`Revenue`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#revenue-class) object and send it with [`revenue()`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#revenue). The SDK sends a `revenue_amount` event that Amplitude's revenue charts read.

```js
import { init, revenue, Revenue } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

const purchase = new Revenue()
  .setProductId("com.example.pro-annual")
  .setPrice(3.99)
  .setQuantity(3)
  .setRevenueType("purchase")
  .setEventProperties({ coupon: "SPRING" });

revenue(purchase, { user_id: "user-12345" });
```

```json
{
  "event_type": "revenue_amount",
  "user_id": "user-12345",
  "event_properties": {
    "coupon": "SPRING",
    "$productId": "com.example.pro-annual",
    "$quantity": 3,
    "$price": 3.99,
    "$revenueType": "purchase"
  }
}
```

Amplitude computes revenue as price times quantity, unless you call `setRevenue()`. The SDK doesn't require a price: a `Revenue` object without `setPrice()` sends `$price: 0`. `setQuantity()` ignores values of `0` or less and keeps the default of `1`.

### Send events to more than one project

Create one instance per Amplitude project with `createInstance()`. Each instance has its own API key, options, queue, and plugins.

```js
import { createInstance } from "@amplitude/analytics-node";

const billing = createInstance();
billing.init("PROJECT_A_API_KEY");

const product = createInstance();
product.init("PROJECT_B_API_KEY", { flushQueueSize: 50 });

billing.track("Invoice Paid", { amount: 42 }, { user_id: "user-12345" });
product.track("Invoice Paid", { amount: 42 }, { user_id: "user-12345" });
```

The named functions, such as `track()`, act on a separate default instance. The `instanceName` option labels an instance but doesn't link instances together.

## Identify users

### How the SDK identifies users

The Node.js SDK sends events on behalf of many users, so it doesn't store a user ID, device ID, or session. Pass `user_id`, `device_id`, or both with every call, in the last argument. Amplitude rejects an event that has neither, with `400` and `Request missing required field`. The SDK doesn't check this before it sends the event.

If your server handles requests from a browser or app that also runs an Amplitude SDK, pass that client's `device_id` and `session_id` to the server. Set them on server events so that Amplitude joins server events to the same user and session.

### Allow shorter IDs

Amplitude rejects user IDs and device IDs shorter than five characters, with `Invalid id length for user_id or device_id`. To accept shorter IDs, set `minIdLength`. The SDK sends the value to Amplitude with each upload. It doesn't check ID length itself.

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", { minIdLength: 3 });

track("Button Clicked", undefined, { user_id: "u42" });
```

### Opt out of tracking

`setOptOut(true)` stops the instance from sending events. It applies to the whole instance, not to one user, and the setting lasts only for the life of the process.

```js
import { init, setOptOut, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

setOptOut(true);
track("Button Clicked", undefined, { user_id: "user-12345" }); // not sent

setOptOut(false); // later events are sent again
```

While opt-out is on, `track()` and the other tracking methods resolve with `0` and `Event skipped due to optOut config`. To skip events for individual users who opted out, check your own consent record before you call `track()`, or drop their events in a [plugin](#drop-events).

## Plugins

Plugins change events before the SDK sends them, or send them to other destinations. A plugin is an object with a `type`, a `name`, and an `execute()` function. Import TypeScript types for plugins from the `Types` namespace of `@amplitude/analytics-node`.

### How plugins run

For each event, the SDK runs plugins in three stages:

1. **`before` plugins**: Run in the order you added them. The built-in `context` plugin is a `before` plugin. It sets `time` and `insert_id` when they're missing, and always sets `event_id` and `library`.
2. **`enrichment` plugins**: Run in the order you added them. A plugin without a `type` is an enrichment plugin.
3. **`destination` plugins**: Run all at once, each with its own copy of the event. The built-in `amplitude` destination uploads to Amplitude.

A `before` or `enrichment` plugin returns the event, changed or not, or `null` to drop it. Plugins you add before `init()` finishes run ahead of the `context` plugin, so those plugins don't see `time`, `insert_id`, or `library` yet. To see those fields, add the plugin after `await init(...).promise`, or use an enrichment plugin.

`track()` resolves with the result of the first destination plugin the instance registered. Add your own destinations after `init()` resolves, so the result reflects Amplitude's response.

### Add or remove a plugin

```js
import { init, add, remove, track } from "@amplitude/analytics-node";

await init("AMPLITUDE_API_KEY").promise;

const tagEnvironment = {
  name: "tag-environment",
  type: "enrichment",
  execute: async (event) => {
    event.event_properties = {
      ...event.event_properties,
      environment: "production",
    };
    return event;
  },
};

add(tagEnvironment);
track("Button Clicked", undefined, { user_id: "user-12345" });

// Later: remove the plugin by name.
remove("tag-environment");
```

Each plugin's `name` must be unique on the instance. The SDK skips a plugin whose name is already registered and logs `Plugin with name tag-environment already exists, skipping registration`.

### Change events before other plugins

A `before` plugin runs ahead of every enrichment and destination plugin. This example plugin lowercases `user_id`, so later plugins and Amplitude see the normalized value:

```ts
import { add, init, track, Types } from "@amplitude/analytics-node";

const normalizeUserId: Types.BeforePlugin = {
  name: "normalize-user-id",
  type: "before",
  execute: async (event: Types.Event) => {
    if (event.user_id) {
      event.user_id = event.user_id.toLowerCase();
    }
    return event;
  },
};

add(normalizeUserId);
init("AMPLITUDE_API_KEY");

track("Button Clicked", undefined, { user_id: "User-12345" });
```

### Enrich events

An enrichment plugin is a class or object with an `execute()` method. The optional `setup(config, client)` runs once, when you add the plugin or when `init()` finishes, whichever is later. Use it to read the configuration or prepare state.

```ts
import { add, init, track, Types } from "@amplitude/analytics-node";

class ServiceInfoPlugin implements Types.EnrichmentPlugin {
  name = "service-info";
  type = "enrichment" as const;
  private region = "unknown";

  async setup(config: Types.IConfig) {
    this.region = config.serverZone === "EU" ? "eu" : "us";
  }

  async execute(event: Types.Event) {
    event.event_properties = {
      ...event.event_properties,
      service: "billing-worker",
      amplitude_region: this.region,
    };
    return event;
  }
}

await init("AMPLITUDE_API_KEY").promise;
add(new ServiceInfoPlugin());

track("Job Completed", { jobId: "job-77" }, { user_id: "user-12345" });
```

### Drop events

Return `null` from a `before` or `enrichment` plugin to drop an event. The dropped call resolves with code `0` and an empty message.

```js
import { init, add, track } from "@amplitude/analytics-node";

await init("AMPLITUDE_API_KEY").promise;

add({
  name: "drop-health-checks",
  type: "enrichment",
  execute: async (event) =>
    event.event_type === "Health Check" ? null : event,
});

track("Health Check", undefined, { user_id: "user-12345" }); // dropped
track("Button Clicked", undefined, { user_id: "user-12345" }); // sent
```

### Send events to another destination

A destination plugin receives a copy of every event and must return a result with `event`, `code`, and `message`. This example plugin posts each event to your own endpoint with the global `fetch()`:

```ts
import { add, init, track, Types } from "@amplitude/analytics-node";

class WebhookDestination implements Types.DestinationPlugin {
  name = "webhook-destination";
  type = "destination" as const;

  constructor(private readonly url: string) {}

  async execute(event: Types.Event): Promise<Types.Result> {
    try {
      const response = await fetch(this.url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(event),
      });
      return { event, code: response.status, message: response.statusText };
    } catch (error) {
      return { event, code: 0, message: String(error) };
    }
  }
}

await init("AMPLITUDE_API_KEY").promise;
add(new WebhookDestination("https://collector.example.com/events"));

track("Button Clicked", undefined, { user_id: "user-12345" });
```

## Data delivery

### How the SDK uploads events

The SDK queues events in memory and uploads them in the background:

- **Timing**: When an event enters an empty queue, the SDK schedules an upload `flushIntervalMillis` later (10 seconds by default). The pending upload keeps the Node.js process alive until it runs.
- **Batch size**: Each request holds up to `flushQueueSize` events (200 by default). The SDK sends larger queues as several requests, one after another. Reaching `flushQueueSize` doesn't start an upload early.
- **Storage**: The queue lives only in memory. The SDK loses events that haven't uploaded when the process exits or crashes, unless you set a [`storageProvider`](#keep-unsent-events-across-restarts).
- **Compression**: The SDK sends plain JSON and doesn't compress request bodies.

Scheduled uploads retry failures. `flushMaxRetries` (12 by default) is the total number of attempts for an event, including the first.

| Response | What the SDK does |
| --- | --- |
| `2xx` | Resolves each event with the status and `Event tracked successfully`. |
| `400` for some events | Drops the events Amplitude names as invalid, and retries the rest on the next upload. |
| `400` for a missing or invalid API key | Drops every event in the request. |
| `413` | Halves `flushQueueSize` for the rest of the instance's life and retries. Drops a single event that's too large. |
| `429` | Drops events for users or devices over their daily quota. Waits 30 seconds before it retries throttled events. |
| `408` (request timeout), `5xx`, or a broken response | Retries after a backoff that grows by one second per attempt. |
| No response (DNS, connection refused) | Drops the events with `Unexpected error occurred`, without retrying. |

An upload you start with [`flush()`](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#flush) works differently: it makes one attempt per batch and doesn't retry. Events in a retry backoff stay queued for the next scheduled upload. If a scheduled upload is still in progress, `flush()` does nothing and resolves.

### Tune upload performance

Raise `flushQueueSize` to send fewer, larger requests. Lower `flushIntervalMillis` to send events sooner.

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", {
  flushQueueSize: 500, // events per request
  flushIntervalMillis: 2000, // upload 2 seconds after the first queued event
  flushMaxRetries: 5, // attempts per event, including the first
});

track("Button Clicked", undefined, { user_id: "user-12345" });
```

`0` isn't a valid value for `flushQueueSize` or `flushMaxRetries`: the SDK uses the default instead.

### Choose a data region

If your Amplitude project is in the EU data center, set `serverZone` to `"EU"`. The SDK then sends to `https://api.eu.amplitude.com/2/httpapi`. Use the API key from your EU project.

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", { serverZone: "EU" });

track("Button Clicked", undefined, { user_id: "user-12345" });
```

### Send large batches to the Batch API

For high-volume backfills, set `useBatch` to `true`. The SDK then sends to the [Batch Event Upload API](https://amplitude.com/docs/apis/analytics/batch-event-upload) at `https://api2.amplitude.com/batch`, or `https://api.eu.amplitude.com/batch` with `serverZone: "EU"`. Batching, intervals, and retries stay the same.

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", { useBatch: true, flushQueueSize: 1000 });

track("Record Imported", { source: "crm" }, { user_id: "user-12345" });
```

### Send events through a proxy

Set `serverUrl` to your proxy's URL. A custom `serverUrl` replaces the URL that `serverZone` and `useBatch` select. Point `serverUrl` at the endpoint you want, and have the proxy forward requests to Amplitude unchanged.

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", {
  serverUrl: "https://analytics-proxy.example.com/2/httpapi",
});

track("Button Clicked", undefined, { user_id: "user-12345" });
```

The SDK makes its own HTTPS requests with Node.js's `https` module and doesn't read `HTTP_PROXY` or `HTTPS_PROXY` itself. To send through a forward proxy that requires those settings, use a [custom HTTP client](#use-a-custom-http-client).

### Set a request timeout

The SDK aborts an upload that takes longer than `requestTimeoutMillis` (10 seconds by default) and retries it like a `408` response. The timeout is a deadline for the whole request, not an idle timer, so a slow response that keeps sending data still times out. Requires 1.5.70 or later. Earlier versions have no request timeout.

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", { requestTimeoutMillis: 5000 });

track("Button Clicked", undefined, { user_id: "user-12345" });
```

`requestTimeoutMillis` applies only to the built-in HTTP client. With `transportProvider`, your client handles timeouts.

### Use a custom HTTP client

Set `transportProvider` to replace the built-in HTTP client, for example to add headers or send through a proxy agent. Extend `BaseTransport` from `@amplitude/analytics-core`, which the SDK installs as a dependency, and convert the server's JSON response with `buildResponse()`. If your package manager doesn't hoist dependencies, add `@amplitude/analytics-core` to your own `package.json`.

```ts
import { init, track } from "@amplitude/analytics-node";
import {
  BaseTransport,
  type Payload,
  type Response,
} from "@amplitude/analytics-core";

class HeaderTransport extends BaseTransport {
  async send(serverUrl: string, payload: Payload): Promise<Response | null> {
    try {
      const response = await fetch(serverUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Request-Source": "billing-worker",
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(10_000),
      });
      return this.buildResponse(await response.json());
    } catch {
      return null; // the SDK drops the events without retrying
    }
  }
}

init("AMPLITUDE_API_KEY", { transportProvider: new HeaderTransport() });

track("Button Clicked", undefined, { user_id: "user-12345" });
```

Return `null` only when the request never reached a server. To make the SDK retry, return `this.buildResponse({ code: 408 })` instead.

### Keep unsent events across restarts

By default, the SDK keeps its queue only in memory. To keep unsent events when the process restarts, set `storageProvider` to an object that implements the [`Storage` interface](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#storage). The SDK saves the queue under the key `AMP_unsent_` plus the first 10 characters of your API key, and resends saved events after `init()`.

```js
import { readFile, writeFile, rm } from "node:fs/promises";
import { init, track } from "@amplitude/analytics-node";

const fileStorage = {
  file: (key) => `/tmp/${key}.json`,
  isEnabled: async () => true,
  getRaw: async (key) =>
    readFile(fileStorage.file(key), "utf8").catch(() => undefined),
  get: async (key) =>
    JSON.parse((await fileStorage.getRaw(key)) ?? "null") ?? undefined,
  set: async (key, value) =>
    writeFile(fileStorage.file(key), JSON.stringify(value)),
  remove: async (key) => rm(fileStorage.file(key), { force: true }),
  reset: async () => {},
};

// Await init(): with an asynchronous storage, events tracked before init() finishes are discarded.
await init("AMPLITUDE_API_KEY", { storageProvider: fileStorage }).promise;

track("Button Clicked", undefined, { user_id: "user-12345" });
```

## Logging

The SDK logs warnings and errors to the console by default. To route its messages to your own logger, set `loggerProvider` to an object with the methods of the [`Logger` interface](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/reference#logger), and set `logLevel` to the lowest level you want:

```js
import { init, track, Types } from "@amplitude/analytics-node";

let level = Types.LogLevel.None;
const appLogger = {
  disable: () => (level = Types.LogLevel.None),
  enable: (logLevel) => (level = logLevel),
  log: (...args) =>
    level >= Types.LogLevel.Verbose && console.info("[amplitude]", ...args),
  warn: (...args) =>
    level >= Types.LogLevel.Warn && console.warn("[amplitude]", ...args),
  error: (...args) =>
    level >= Types.LogLevel.Error && console.error("[amplitude]", ...args),
  debug: (...args) =>
    level >= Types.LogLevel.Debug && console.debug("[amplitude]", ...args),
};

init("AMPLITUDE_API_KEY", {
  loggerProvider: appLogger,
  logLevel: Types.LogLevel.Warn,
});

track("Button Clicked", undefined, { user_id: "user-12345" });
```

The SDK calls `enable(logLevel)` during `init()`. The logger filters by level, not the SDK, so apply the level in your methods, as the example does. For what each level includes, go to [Turn on debug logging](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/verify-events#turn-on-debug-logging).

## Use the SDK in request handlers

Initialize the SDK once when your server starts, then call `track()` in route handlers without awaiting it. The SDK uploads in the background.

```js
import express from "express";
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

const app = express();
app.use(express.json());

app.post("/orders", async (req, res) => {
  const order = await createOrder(req.body);

  // Don't await: the upload happens in the background.
  track("Order Created", { orderId: order.id }, { user_id: req.user.id });

  res.status(201).json(order);
});

app.listen(3000);
```

Awaiting `track(...).promise` in a handler delays the response until the next scheduled upload finishes: up to `flushIntervalMillis` (10 seconds by default), plus network time and any retries.

If a handler must know the upload outcome before it responds, call `flush()` and then await the event's promise. Initialization finished long before the request arrived, so `flush()` starts an upload right away:

```js
app.post("/orders", async (req, res) => {
  const order = await createOrder(req.body);

  const pending = track(
    "Order Created",
    { orderId: order.id },
    { user_id: req.user.id },
  );
  await flush().promise;
  const result = await pending.promise; // result.code and result.message

  res.status(201).json({ order, analytics: result.code });
});
```

`flush()` makes one attempt and doesn't retry, so a failure is final for the flushed events.

## Serverless and short-lived processes

Serverless functions, CLI scripts, and job workers can end before the SDK's next upload. Await `init()` once, then await `flush().promise` before the function returns or the script exits.

```js
import { init, track, flush } from "@amplitude/analytics-node";

// Runs once per container or process.
const ready = init("AMPLITUDE_API_KEY").promise;

export async function handler(job) {
  await ready;

  track("Job Completed", { jobId: job.id }, { user_id: job.userId });

  await flush().promise;
  return { statusCode: 200 };
}
```

To try the handler locally, call it once:

```js
await handler({ id: "job-77", userId: "user-12345" });
```

Two rules keep events from being lost:

- **Await `init()` before `flush()`.** If `flush()` runs before `init()` finishes, the queued events resolve with `100` and `Event not tracked, no destination plugins on the instance`, and the SDK never sends them.
- **Await the flush.** A frozen or ended process can't finish the upload. Because `flush()` doesn't retry, check the event results if delivery matters, and track again on failure.

