typescript 124 lines · 3 tabs

Batching Analytics Events With Interval Flush and Backpressure in TypeScript

Shared by codesnips Aug 2026
3 tabs
export interface AnalyticsEvent {
  name: string;
  props?: Record<string, unknown>;
  ts: number;
}

export interface Transport {
  send(batch: AnalyticsEvent[]): Promise<boolean>;
}

interface QueueOptions {
  batchSize: number;
  maxBufferSize: number;
  transport: Transport;
}

export class EventQueue {
  private buffer: AnalyticsEvent[] = [];
  private flushing = false;
  dropped = 0;

  constructor(private readonly opts: QueueOptions) {}

  enqueue(event: AnalyticsEvent): void {
    this.buffer.push(event);
    if (this.buffer.length > this.opts.maxBufferSize) {
      const overflow = this.buffer.length - this.opts.maxBufferSize;
      this.buffer.splice(0, overflow);
      this.dropped += overflow;
    }
    if (this.buffer.length >= this.opts.batchSize) {
      void this.flush();
    }
  }

  async flush(): Promise<void> {
    if (this.flushing || this.buffer.length === 0) return;
    this.flushing = true;
    try {
      while (this.buffer.length > 0) {
        const batch = this.buffer.splice(0, this.opts.batchSize);
        const ok = await this.opts.transport.send(batch);
        if (!ok) {
          this.buffer.unshift(...batch);
          break;
        }
      }
    } finally {
      this.flushing = false;
    }
  }
}
3 files · typescript Explain with highlit

This snippet shows a small client-side analytics pipeline that buffers events in memory and flushes them to a collector endpoint in batches, either when a size threshold is reached or when a periodic timer fires. Batching matters because sending one HTTP request per track call is wasteful and slow; coalescing many events into a single request reduces overhead, and an interval flush guarantees events still leave the buffer even during quiet periods.

In EventQueue, the core is a bounded FIFO array buffer guarded by maxBufferSize. When enqueue is called the event is pushed; if the buffer overflows the oldest entries are dropped and counted in dropped so telemetry loss is observable rather than silent. This is the backpressure trade-off: under a burst the queue favors bounded memory over completeness. When the buffer reaches batchSize, enqueue triggers flush immediately instead of waiting for the timer, keeping latency low for active sessions. The flush method splices out a batch, calls the injected transport, and on failure re-queues the events at the front so a transient network error does not lose data. A flushing guard prevents overlapping flushes from racing on the same buffer.

BeaconTransport implements the actual send. It prefers navigator.sendBinary-style fetch with keepalive so requests survive brief navigations, and falls back to navigator.sendBeacon during page unload, which the browser delivers reliably even as the tab closes. The send method returns a boolean the queue uses to decide whether to re-queue.

Analytics wires everything together: it constructs the queue with a transport, starts a setInterval timer in start, and registers a visibilitychange listener so a final flush runs when the page is hidden — the single most common moment to lose buffered events. track is the public API that stamps each event with a timestamp and forwards it to the queue. stop clears the timer and performs one last drain, making the lifecycle explicit. Together the files demonstrate durable batching: bounded buffers, immediate flush on threshold, periodic flush on idle, retry on failure, and a guaranteed flush on unload.


Related snips

Share this code

Here's the card — post it anywhere.

Batching Analytics Events With Interval Flush and Backpressure in TypeScript — share card
Link copied