---
sdk = "node-sdk"
previous_release_version = "6.0.0"
---

# Node SDK 6.1.0

## Highlights

- **`@xmtp/node-sdk` now ships from the [libxmtp](https://github.com/xmtp/libxmtp) monorepo**, released in lockstep with `@xmtp/node-bindings`. The package name and install instructions are unchanged.
- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate.
- **The `send*` helpers take an options object** instead of a bare `isOptimistic` boolean — the one breaking change in this release.
- **Database pool and single-connection options**, for services running many clients in one process.
- **OpenTelemetry export of both spans and logs**, plus a standalone `initLogging()` you can call before creating a client.
- **Background worker tuning** via `workerConfig` — per-worker intervals, jitter, and enable/disable.

## What's Changed

Sends now carry an optional idempotency key, and the per-content-type `send*` helpers take a `SendOpts` object so optimistic delivery and the idempotency key can be set together. `Client.create` and `Client.build` also correctly preserve the `{ backend }` options arm when you pass codecs — previously TypeScript collapsed that union and rejected the option.

Underneath, the two API connections each client used to open have been collapsed into one, background workers are configurable, logging can export spans and logs over OTLP with a separate stdout level, and the local database can run on a single connection instead of a pool.

This release also picks up the reliability fixes in `@xmtp/node-bindings` 1.11.0 — see the [Bug Fixes](#bug-fixes) section.

## Breaking Changes

### The `send*` helpers take a `SendOpts` object instead of `isOptimistic`

**This is a breaking change.**

```ts
// Before
await conversation.sendText("gm", true);

// After
await conversation.sendText("gm", { optimistic: true });
```

This applies to `sendText`, `sendMarkdown`, `sendReaction`, `sendReadReceipt`, `sendReply`, `sendAttachment`, `sendRemoteAttachment`, `sendMultiRemoteAttachment`, `sendTransactionReference`, `sendWalletSendCalls`, `sendActions`, and `sendIntent`.

`SendOpts` is exported from the package:

```ts
import type { SendOpts } from "@xmtp/node-sdk";
```

## New Features

### Idempotency keys on sends

Both `SendMessageOpts` (on `conversation.send`) and `SendOpts` (on the `send*` helpers) accept `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted.

```ts
await conversation.sendText("gm", { idempotencyKey: "greeting-2026-07-28" });
```

### Database pool configuration and single-connection mode

`ClientOptions` gained three storage options:

- `maxDbPoolSize` — maximum connections in the local database pool (default 25).
- `minDbPoolSize` — connections kept warm (default 5).
- `useSingleConnection` — when `true`, the native database uses one connection (one file descriptor) instead of a pool, and the pool-size options are ignored. Intended for services running many clients in one process.

### Telemetry and logging

- `otelEndpoint` — e.g. `"http://collector:4317"`. Spans **and** log events are exported via OTLP, so a downstream OpenTelemetry Collector can derive metrics from the spans and forward the correlated logs.
- `resourceAttributes` — attributes attached to all exported telemetry, e.g. `{ "service.instance.id": "worker-7" }`.
- `stdoutLoggingLevel` — level for the stdout console layer only. Defaults to `loggingLevel`. Set it to `LogLevel.Warn` to quiet stdout below the OTLP export level so a log shipper does not duplicate logs already exported.
- `initLogging(options?)` is now re-exported. `await` it before creating any client to install the logging pipeline early; it is idempotent, so passing `logOptions`-style fields to `Client.create` afterwards is still safe.
- Call `flushTelemetry()` on graceful shutdown to flush buffered spans.

### Background worker configuration

`ClientOptions.workerConfig` tunes the background worker scheduler — a global default interval, per-worker interval and jitter overrides, and a list of workers to disable. Worker kinds: `DeviceSync`, `DisappearingMessages`, `KeyPackageCleaner`, `CommitLog`, `TaskRunner`.

```ts
const client = await Client.create(signer, {
  env: "production",
  workerConfig: {
    workerIntervalsNs: [{ kind: "DeviceSync", intervalNs: 30_000_000_000n }],
    disabledWorkers: ["DisappearingMessages"],
  },
});
```

`WorkerJitterOverride` is now re-exported alongside `WorkerIntervalOverride` and `WorkerConfigOptions`.

### Wait for registration to become visible on the network

`ClientOptions.waitForRegistrationVisible` accepts `{ quorumPercentage?, quorumAbsolute?, timeoutMs? }`. When set, client creation waits until a quorum of nodes confirms the new identity and key package are visible before returning, which avoids racing a just-registered installation against propagation.

### gRPC keepalive is tunable via environment variables

For deployments on high-latency paths where the default fast keepalive causes spurious `UNAVAILABLE` disconnects. Defaults are unchanged, and no code change is needed — set the variables in your process environment.

| Variable | Default |
| --- | --- |
| `XMTP_GRPC_KEEPALIVE_INTERVAL_SECS` | 16 |
| `XMTP_GRPC_KEEPALIVE_TIMEOUT_SECS` | 10 |
| `XMTP_GRPC_TCP_KEEPALIVE_SECS` (0 disables) | 16 |
| `XMTP_GRPC_KEEPALIVE_WHILE_IDLE` | true |

### Opt-in stream-liveness watchdog

A subscription whose transport wedges open — an L7 proxy that keeps answering HTTP/2 pings while the backend subscription is gone — used to deliver neither an error nor a close, and the consumer just hung. There is now an idle timer per stream that, when nothing arrives within the timeout, tears the stream down and re-subscribes from the persisted cursor.

It is **off by default**: with no env vars set, streams behave exactly as before. Long-running agents can opt in.

| Variable | Default |
| --- | --- |
| `XMTP_STREAM_WATCHDOG_ENABLED` | false |
| `XMTP_STREAM_WATCHDOG_IDLE_TIMEOUT_SECS` | 300 |
| `XMTP_STREAM_WATCHDOG_RECONNECT_BASE_SECS` | 1 |
| `XMTP_STREAM_WATCHDOG_RECONNECT_JITTER_MS` (0 disables) | 1000 |

Because there is no server-side keepalive on this path yet, the idle timeout is deliberately long — a healthy but dormant stream will also trip and reconnect roughly once per timeout. Values are read once per process at first use.

## Bug Fixes

- **`Client.create` / `Client.build` rejected the `{ backend }` option when codecs were passed.** `Omit` collapsed the options union and dropped the `backend` arm; a distributive omit now preserves it.
- **Device sync panic on some platforms.** The rustls crypto provider is now installed explicitly instead of relying on a static constructor that does not run everywhere, which caused a "No provider set" panic in the device-sync worker's HTTP client.
- **Crash on the welcome stream** after a group's creator left the group and was later re-added.
- **False-positive fork detection** after a member was removed from a group.
- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered.
- **Cache read returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch lookup could return unrelated rows.
- **Intentional transaction rollbacks** are no longer reported as telemetry errors, and database disconnect errors now propagate out of workers to the supervisor instead of being swallowed.
- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync.
- **New installations reliably added to existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group.
- **Downgrade resilience for group intents.** Intent rows written by a newer version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group.
- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported.
- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in the sync summary rather than replaced with a generic error.
- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing.
- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused.
- **`GetNodes` DNS flakes** are now retried instead of failing the call.
- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers.
