Batched mode
Batched mode buffers events client-side and flushes them in batches to the /events/batch endpoint. It exists for one situation: a hot emit path where the per-call HTTP overhead would otherwise dominate. Outside that situation, the default unbuffered mode is simpler and just as correct.
When buffered makes sense
Buffered mode helps when your code emits many events per second on a small number of processes, a request handler that emits 5 events per request at 1000 RPS, a worker that processes a queue of records and emits one event each, an in-process event bus that fans out to Pushrail.
It does not help when the emit rate is low (a couple of events per second per process, the overhead is invisible) or when the events are already produced in batches (just call emitBatch directly).
Enabling
Pass buffered: true to the constructor:
import { Pushrail } from "@pushrail/sdk";
const pushrail = new Pushrail({
apiKey: process.env.PUSHRAIL_API_KEY!,
buffered: true,
maxBatchSize: 500, // default
flushIntervalMs: 1_000, // default, flush after this many ms of inactivity
onBufferError: (err, events) => {
// Called when a buffered batch fails permanently after retries.
logger.error({ err, eventCount: events.length }, "pushrail buffer error");
},
});
Once enabled, events.emit and events.emitBatch go through the buffer. The return value is still a Promise<EmitResult> that resolves after the event has been flushed (successfully or with a terminal error).
Flush triggers
The buffer flushes on three conditions:
- Size trigger: when pending events reach
maxBatchSize, the buffer flushes immediately. - Time trigger: after
flushIntervalMsof no new events, the buffer flushes whatever is pending. - Explicit flush: calling
pushrail.flush()orpushrail.events.flushBuffer()flushes the current contents immediately.
Each flush sends up to 100 events per HTTP request (the server batch cap). If the pending count exceeds 100, the buffer makes multiple back-to-back requests inside one flush cycle.
Backpressure / dropping behavior
If a flush is already in flight and the pending backlog reaches maxBatchSize * 2, the buffer signals backpressure. New emit calls fall back to a direct synchronous POST instead of enqueueing. This preserves at-least-once delivery semantics even when the buffer is saturated. The fall-through is transparent to your code; the call still returns a normal EmitResult.
The buffer never silently drops events. If a batch fails after the SDK's retry budget is exhausted, the onBufferError callback fires with the failed events so your code can persist them to a dead-letter store, alert, or escalate.
Graceful shutdown
Buffered mode adds one responsibility: flush before the process exits. Call pushrail.close() (or the alias pushrail.flush() if you only need to drain without tearing down the client) before your shutdown completes.
process.on("SIGTERM", async () => {
logger.info("draining pushrail buffer");
await pushrail.close();
process.exit(0);
});
In containerized deployments, make sure your container runtime sends SIGTERM and waits at least flushIntervalMs + (network RTT × maxRetries) before sending SIGKILL. For default settings, 30 seconds is a comfortable margin.
Forgetting to call close() on shutdown is the most common buffered-mode bug. Up to maxBatchSize events can be in the buffer when your process dies, and those events are lost. Wire it into your shutdown hook the first time you enable buffering.