typescript
50 lines · 8 steps
Batching analytics events in TypeScript
An EventBatcher buffers events in memory and flushes them to a server by size or on a timer, requeuing on failure.
Explained by
highlit
1type Event = Record<string, unknown>;
2
3interface BatcherOptions {
4 endpoint: string;
5 maxBatchSize?: number;
6 flushIntervalMs?: number;
7}
8
9export class EventBatcher {
10 private queue: Event[] = [];
11 private timer: ReturnType<typeof setInterval> | null = null;
12 private readonly maxBatchSize: number;
13
14 constructor(private readonly options: BatcherOptions) {
15 this.maxBatchSize = options.maxBatchSize ?? 50;
16 this.timer = setInterval(() => {
17 void this.flush();
18 }, options.flushIntervalMs ?? 5000);
19 }
20
21 track(event: Event): void {
22 this.queue.push({ ...event, ts: Date.now() });
23 if (this.queue.length >= this.maxBatchSize) {
24 void this.flush();
25 }
26 }
27
28 async flush(): Promise<void> {
29 if (this.queue.length === 0) return;
30
31 const batch = this.queue.splice(0, this.queue.length);
32 try {
33 await fetch(this.options.endpoint, {
34 method: "POST",
35 headers: { "content-type": "application/json" },
36 body: JSON.stringify({ events: batch }),
37 keepalive: true,
38 });
39 } catch (err) {
40 this.queue.unshift(...batch);
41 console.error("event flush failed, requeued", err);
42 }
43 }
44
45 async close(): Promise<void> {
46 if (this.timer) clearInterval(this.timer);
47 this.timer = null;
48 await this.flush();
49 }
50}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Batching trades a little latency for far fewer network round-trips.
- 2Flushing on both a size threshold and a timer bounds memory and delay at once.
- 3Requeuing a failed batch preserves data at the cost of possible duplicate delivery.
Related explainers
javascript
const logger = require('./logger'); class AppError extends Error { constructor(message, statusCode = 500, details = null) {
Centralized error handling in Express
error-handling
middleware
custom-errors
Intermediate
8 steps
typescript
import { Injectable } from '@angular/core'; import { PreloadingStrategy, Route, Routes, RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; import { Observable, of, timer } from 'rxjs';
A custom preloading strategy in Angular
lazy-loading
preloading
rxjs
Intermediate
7 steps
php
<?php namespace App\Services;
Resilient HTTP retries with Laravel's client
http-client
retry-logic
error-handling
Intermediate
7 steps
rust
use std::fs::File; use std::io::{self, BufWriter}; use std::path::Path;
Serializing config to JSON in Rust
serialization
serde
file-io
Intermediate
7 steps
typescript
import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq'; import { Logger } from '@nestjs/common'; import { Job } from 'bullmq'; import { MailerService } from '../mailer/mailer.service';
Processing background email jobs in NestJS
job-queue
background-processing
dependency-injection
Intermediate
7 steps
typescript
import { Component, computed, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { CommonModule } from '@angular/common';
Building a signup stepper in Angular
signals
reactive-forms
form-validation
Intermediate
10 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/batching-analytics-events-in-typescript-explained-typescript-2bc0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.