paperclipai/paperclip · warning

dropping batch ${batch.batchId} on terminal response (HTTP $

Error message

dropping batch ${batch.batchId} on terminal response (HTTP ${outcome.status}); ${batch.events.length} event(s) lost

What it means

attemptSend classifies the HTTP outcome for each telemetry batch. A 'terminal' status — a non-retryable client error such as 400/401/403/404/410 — means retrying cannot help, so the batch is dropped and logged. Dropped batches are never replayed; telemetry is deliberately best-effort so it never blocks the host app.

Source

Thrown at packages/shared/src/telemetry/client.ts:268

      // as effectively unbounded so `splitByBytes` routes it to the existing
      // over-limit drop-and-log path instead of throwing out of the flush.
      return Number.POSITIVE_INFINITY;
    }
  }

  /**
   * Sends one batch (its current `attempt`). On a retryable failure the EXACT
   * same events + `batchId` are re-queued with capped, jittered backoff; on a
   * terminal failure or after `maxAttempts` the batch is dropped-and-logged.
   */
  private async attemptSend(batch: PendingBatch): Promise<void> {
    const body = JSON.stringify(this.buildEnvelope(batch.events, batch.batchId));
    const outcome = this.classifyOutcome(await this.postEnvelope(body));

    if (outcome.kind === "ok") return;

    if (outcome.kind === "terminal") {
      this.warn(
        `dropping batch ${batch.batchId} on terminal response (HTTP ${outcome.status}); ${batch.events.length} event(s) lost`,
      );
      return;
    }

    // Retryable (429/502/503/504 or network/timeout).
    if (batch.attempt >= this.caps.backoff.maxAttempts) {
      this.warn(
        `dropping batch ${batch.batchId} after ${batch.attempt} attempt(s); ${batch.events.length} event(s) lost`,
      );
      return;
    }
    // Cap the delay at maxDelayMs. `computeBackoffMs` is already capped, but a
    // server `Retry-After` hint is not — an out-of-range value could otherwise
    // overflow the runtime timer range and be clamped by Node to a near-immediate
    // timeout, causing rapid retries. The cap keeps every retry within the
    // configured backoff ceiling.
    const delayMs = Math.min(

View on GitHub (pinned to 120ae5428f)

Solutions

  1. Read the HTTP status from the warning and fix the root cause: correct URL, valid token, or matching envelope schema.
  2. Reproduce with curl: POST a minimal sample envelope to the collector with the same auth and inspect the response.
  3. Align the client envelope schema with the collector version you run.
  4. After the fix, confirm drop warnings stop — events lost before the fix are gone.

Example fix

// before
telemetry: { url: 'https://collector.example/ingest' }

// after
telemetry: { url: 'https://collector.example/api/v2/telemetry' }
Defensive patterns

Strategy: validation

Validate before calling

// Startup canary: one synthetic envelope must be accepted before telemetry matters.
const res = await fetch(collectorUrl, {
  method: 'POST',
  headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
  body: JSON.stringify({ batchId: 'canary', events: [{ name: 'canary', ts: Date.now(), dims: {} }] }),
});
if (!res.ok) throw new Error(`Telemetry collector rejected canary: HTTP ${res.status}`);

Prevention

When it happens

Trigger: The collector rejects the POST with a client error: wrong endpoint path (404), invalid/expired auth token (401/403), envelope rejected as malformed (400), or a decommissioned ingestion route (410).

Common situations: Rotated telemetry API keys deployed to only one side; proxy or gateway rewriting URLs; collector upgraded to a new envelope schema; misconfigured base URL after migration.

Related errors


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/92eca9813cc0f0ec. Report an issue: GitHub.