Logstreem
Batching and buffering

Batching and buffering

How to size batches, when to flush, and what to do when ingest is unreachable.

One event per HTTP request works, and will be the thing that breaks first. Batching is the difference between 6,000 requests a minute and six.

Sensible targets

SettingStart atWhy
Batch size1,000 eventsWell under the 10,000 cap, still one round trip
Flush interval2 secondsBounded lag without a request per event
Max payload4 MB compressedHalf the 10 MB cap, leaves headroom for a fat event
Compressiongzip, alwaysStructured logs compress 8–15x; it is free latency

Flush on whichever comes first

Node
const MAX_EVENTS = 1000;
const MAX_WAIT_MS = 2000;

let buffer: unknown[] = [];
let timer: NodeJS.Timeout | null = null;

export function log(event: unknown) {
  buffer.push(event);
  if (buffer.length >= MAX_EVENTS) return flush();
  timer ??= setTimeout(flush, MAX_WAIT_MS);
}

async function flush() {
  if (timer) { clearTimeout(timer); timer = null; }
  if (!buffer.length) return;

  const batch = buffer;
  buffer = [];

  try {
    await fetch("https://api.logstreem.com/v1/ingest/api-gateway-prod", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.LOGSTREEM_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(batch),
    });
  } catch {
    // Bounded retry buffer: drop oldest rather than grow without limit.
    buffer = [...batch.slice(-5000), ...buffer];
  }
}

// Do not lose the tail on shutdown.
process.on("beforeExit", flush);

Bound your retry buffer

An unbounded in-memory buffer during an ingest outage is how a logging library takes down the service it was meant to observe. Cap it, drop the oldest, and count the drops.

Ordering does not matter

Events are placed by their _time, not by arrival order. You can retry a failed batch after later batches have landed and queries still read correctly — see events.