Buffer and Flush Metrics in Serverless Edge Code: 2026 Guide
Metrics collection in serverless edge code works nothing like it does on a traditional server. There’s no background thread quietly draining a queue every 10 seconds. There’s no process that stays alive between requests. The isolate runs your handler, sends the response, and gets frozen or destroyed. Any metric data still sitting in memory at that point is gone.
This is why understanding how to buffer and flush metrics in serverless edge code is not optional. It’s the difference between having observability and flying blind.
Want to skip the theory and start collecting metrics? Check out the Distlang Metrics quickstart to go from code to dashboard in minutes.
What Do “Buffer” and “Flush” Mean for Metrics?
These two terms always appear as a pair, and understanding them separately makes the whole pattern click.
Buffering is the act of accumulating metric data points (counter increments, histogram observations, timing values) in an in-memory array or queue during your handler’s execution. Instead of firing off a network request every time you record a data point, you collect them locally.
Flushing is the act of transmitting that entire accumulated buffer to your metrics backend in a single batch operation. The flush is timed to happen before the runtime kills your execution context.
On a long-running Node.js server, this distinction barely matters. A background thread or interval timer handles export continuously. The process sticks around, so there’s always time to drain the buffer. In serverless edge code, that assumption breaks completely.
The buffer is your staging area. The flush is your last chance to get data out. Miss the flush window, and everything in the buffer vanishes.
For a deeper look at how to configure these behaviors in practice, see our guide on configuring buffer and flush behavior in the JS client.
Why Serverless Edge Code Needs Explicit Flush
Three properties of serverless runtimes create the flush problem.
No Long-Lived Process
Serverless functions are stateless and short-lived by design. There’s no persistent process to attach an APM agent to. As the Boundary SDK documentation notes, edge runtimes freeze the isolate between invocations. There is no process.beforeExit, no SIGTERM, no reliable lifecycle hook that fires before shutdown.
Background machinery that works great on servers (buffered writers, flush timers, worker threads, streaming WebSocket connections) all assume a process that sticks around. A function instance can be frozen or torn down the moment your handler returns, stranding whatever was still in a buffer.
Extremely Short Execution Times
A production trace from Microsoft Azure covering 445 million function invocations over 14 days showed a median execution time of just 600 milliseconds. That’s your entire budget: receive request, do work, build response, and somehow get metrics out the door. There’s no room for lazy background export.
Push Is the Only Option
In traditional monitoring, Prometheus scrapes a /metrics endpoint on your server at regular intervals (the pull model). That endpoint doesn’t exist in an ephemeral isolate. The function might not even be running when the scraper comes calling. Push, where your code sends metrics to a backend, is the only viable model for serverless. And push with buffered batching is far more efficient than push-per-data-point.
Practitioners in the prometheus-developers community have flagged this tension directly. One contributor noted that initializing a TCP connection and using remote write to push metrics per FaaS invocation carries an “enormous latency hit” from connection setup, and that it “might already be too slow.” The buffer-then-flush pattern exists precisely as a compromise: accumulate cheaply in memory, then send once.
Runtime Primitives for Flushing
Each major edge/serverless platform provides a mechanism to extend execution past the response. These are the primitives that make buffer and flush metrics in serverless edge code possible.
Cloudflare Workers: ctx.waitUntil()
Cloudflare’s ctx.waitUntil() lets you perform work after returning a response. It can extend execution for up to 30 seconds after the response is sent or the client disconnects.
export default {
async fetch(request, env, ctx) {
// Handle request and build response
const response = new Response("OK");
// Flush metrics after response is sent
ctx.waitUntil(metricsClient.flush());
return response;
}
};
A recent update made this even more convenient. You can now import waitUntil directly from cloudflare:workers, eliminating the need to pass ctx through multiple layers of function calls. Most existing content still only shows the ctx parameter approach, so be aware of both patterns.
Important constraint: the 30-second waitUntil window runs within your Worker’s wall-clock and CPU time budgets. A slow flush still counts against those limits.
For a full walkthrough of these semantics, see our guide on instrumenting Cloudflare Workers with waitUntil.
Vercel / Next.js: after()
For Next.js 15.1 and above, Vercel recommends using the built-in after() function from next/server instead of calling waitUntil() directly. after() lets you schedule work that runs after the response has been sent or prerender has completed.
import { after } from 'next/server';
export async function GET(request) {
const data = await fetchData();
after(async () => {
await metricsClient.flush();
});
return Response.json(data);
}
Under the hood, after() wraps waitUntil(promise), which extends the lifetime of a serverless invocation until all promises passed to it have settled. But there’s a catch. A GitHub discussion flagged that Vercel’s serverless functions don’t keep running after the response is sent if the runtime isn’t configured properly, causing after() to block instead of running asynchronously. Developers need to understand the underlying mechanics, not just copy-paste.
Our article on using after() in Vercel edge functions covers this in detail.
AWS Lambda: Middleware and Decorators
AWS Lambda Powertools takes a different approach. Metrics are stored in a buffer and flushed when calling publishStoredMetrics(), or automatically at the end of the handler when using the logMetrics() decorator or Middy.js middleware.
import { Metrics, logMetrics } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({ namespace: 'MyApp' });
export const handler = logMetrics(metrics)(async (event) => {
metrics.addMetric('OrderProcessed', 'Count', 1);
// Metrics flush automatically when handler completes
return { statusCode: 200 };
});
Cross-Platform Comparison
| Platform | Primitive | Time Budget | Key Constraint |
|---|---|---|---|
| Cloudflare Workers | ctx.waitUntil() or import { waitUntil } from 'cloudflare:workers' |
Up to 30s wall-clock | CPU time varies by plan (50ms to 30s) |
| Vercel / Next.js 15.1+ | after() from next/server |
Varies by function type | No native gRPC; HTTP-based export only |
| AWS Lambda | publishStoredMetrics() / logMetrics() decorator |
Up to 15 min (Lambda max) | Cold start adds to total latency |
No other resource puts these three primitives side by side with their constraints. This table is the quick reference for deciding how to handle buffer and flush metrics in serverless edge code on your platform.
Buffering Strategies
Not all buffering approaches are equal, and the right choice depends on your function’s execution profile.
Per-Request Buffer
The simplest pattern. You create a buffer at the start of your handler, record metrics into it during execution, and flush everything at the end. This works well for most edge functions where each invocation is independent and short-lived.
Advantages: straightforward, no shared state, easy to reason about. Disadvantage: you pay the flush cost on every single request.
Batch Processor Pattern (OpenTelemetry)
If you’re using OpenTelemetry, the BatchSpanProcessor collects spans and exports them in batches on a timer. This is more efficient for high-throughput scenarios, but it creates a real problem in serverless. The batch timer might not fire before the isolate freezes.
The OpenTelemetry SDK documentation explicitly states that BatchSpanProcessor’s forceFlush method “should only be called in cases where it is absolutely necessary, such as when using some FaaS providers that may suspend the process after an invocation, but before the Processor exports the completed spans.”
So if you use batch processing, you must call forceFlush() in your waitUntil or after() callback. Skip it, and spans stay in the buffer forever (which is to say, they disappear).
Simple Processor Pattern (OpenTelemetry)
The SimpleSpanProcessor exports each span immediately when it ends. No batching, no timer, no risk of stranded data. The trade-off is overhead: every span triggers a network call.
For short-lived functions with low span counts, the simple processor is safer. The community strongly recommends it for serverless. For a broader discussion of batching approaches, see our article on batching strategies for short-lived handlers.
When to Choose Each
Short handlers (under 100ms of work) that produce a handful of metrics: per-request buffer or simple processor. Warm, high-throughput functions handling many requests in rapid succession: batch processor with an explicit forceFlush() wired into your shutdown hook. Edge functions that can’t afford per-request latency: buffered batch with a conservative batch size.
Common Mistakes
These are the errors that cause silent data loss. “Silent” is the key word, because unlike a 500 error, a missed flush produces no visible symptom. Your dashboards just show gaps.
Forgetting to Call Flush
The most common mistake. You instrument your code, record metrics, and everything looks correct in local development (where the process stays alive). In production, the isolate freezes before the buffer drains. The fix is always the same: wire your flush into waitUntil, after(), or the equivalent primitive for your platform.
Batch Size Exceeding the Runtime Budget
A batch of 500 records flushing to a slow endpoint can easily exceed Cloudflare’s 30-second waitUntil window. If your flush commonly exceeds the budget, lower the batch size. This is a tuning problem, not a design problem, but it catches people off guard.
Relying on setTimeout or Background Threads
Edge isolates don’t support the same concurrency primitives as Node.js on a server. setTimeout callbacks may never fire if the isolate freezes. Background threads don’t exist. The only reliable way to do post-response work is through the platform’s official primitive.
Assuming gRPC Works on Edge Runtimes
Vercel Edge Runtime and Cloudflare Workers have no native gRPC support. If your metrics exporter defaults to gRPC (as many OpenTelemetry exporters do), it will fail silently or throw. Use HTTP-based OTLP export only. Edge isolates also have no filesystem, so file-based fallback buffers aren’t an option either.
Not Accounting for Flush Time in Your Budget
The flush itself takes time: DNS resolution, TLS handshake, payload transfer, and waiting for the backend’s acknowledgment. This all counts against your wall-clock budget. A function that uses 28 seconds of its 30-second window processing the request leaves only 2 seconds for the flush. That might not be enough. For tips on handling these edge cases, see our guide on debugging missing metrics and ingestion failures.
Flush Failures Happen at Every Level
Even platform-level telemetry isn’t immune. Cloudflare disclosed an incident where, during a roughly 3.5-hour outage, about 55% of the logs they normally send to customers were lost. If the platform itself can lose buffered data during adverse conditions, application-level code needs to be even more deliberate about flush reliability.
This is why buffer and flush metrics in serverless edge code is a design concern, not an afterthought. You need to pick the right batch size, set appropriate timeouts, and accept that some data loss is possible during extreme conditions. The goal is to minimize it through correct use of primitives.
Buffer and Flush with an API-First Metrics Client
The complexity of managing OpenTelemetry processors, configuring gRPC vs. HTTP exporters, and wiring forceFlush() into lifecycle hooks can be significant. An alternative approach is to use a lightweight, API-first metrics client designed specifically for serverless edge code.
The pattern is straightforward: define a metric set (your counters and histograms), record values during handler execution, and call a single flush method inside your platform’s post-response primitive. No agents, no sidecars, no pipeline to maintain.
// Conceptual pattern for any API-first metrics client
const metrics = createMetricSet({ name: "api-requests" });
export default {
async fetch(request, env, ctx) {
metrics.increment("requests_total", { route: "/api/data" });
const start = Date.now();
const response = await handleRequest(request);
metrics.observe("latency_ms", Date.now() - start);
ctx.waitUntil(metrics.flush());
return response;
}
};
This is the approach that Distlang Metrics takes: a lightweight JS client with built-in flush and buffering support, purpose-built for Cloudflare Workers and Vercel environments. The JavaScript client documentation covers the specifics of configuring buffer sizes and flush behavior.
Related Concepts
Understanding buffer and flush metrics in serverless edge code connects to several adjacent topics. For foundational context on what telemetry means and how it relates to metrics, traces, and logs, see our overview of telemetry, MELT, and OpenTelemetry in serverless. If you’re deciding what to actually measure, our guide on counters and histograms in JavaScript covers the two most important metric types for edge functions.
Frequently Asked Questions
What happens if I don’t flush metrics in a serverless function?
The data stays in memory and is lost when the isolate freezes or terminates. There’s no error, no warning, nothing in your logs. Your dashboards simply show missing data points. This is the most common cause of metric gaps in serverless applications.
Should I use SimpleSpanProcessor or BatchSpanProcessor in edge functions?
For most edge functions, SimpleSpanProcessor is the safer choice. It exports each span immediately, eliminating the risk of stranded data. BatchSpanProcessor is more efficient but requires you to explicitly call forceFlush() before the handler exits. If you can’t guarantee a flush window, use the simple processor.
Can I use Prometheus scraping (pull model) with serverless functions?
No. Prometheus scraping requires a long-running /metrics endpoint that the scraper can reach at any time. Serverless functions don’t have persistent endpoints between invocations. Push-based metrics collection with buffered batching is the only viable approach for serverless edge code.
How large should my metric buffer be?
Keep it small enough that the flush completes within your runtime’s time budget. On Cloudflare Workers, you have up to 30 seconds of wall-clock time in waitUntil. If your backend is slow or distant, a batch of 50 to 100 records is often safer than 500. Test the flush duration in production conditions and adjust accordingly.
Does ctx.waitUntil() block the response?
No. The response is sent immediately when you return it from your handler. waitUntil extends the isolate’s lifetime so your flush can complete after the response is already on its way to the client. The user doesn’t experience any additional latency from the flush.
Why can’t I use gRPC to export metrics from edge runtimes?
Edge runtimes like Cloudflare Workers and Vercel Edge Runtime don’t support native gRPC. They run in restricted V8 isolates, not full Node.js environments. Use HTTP-based export (such as OTLP over HTTP) instead. Many OpenTelemetry exporters default to gRPC, so you’ll need to configure them explicitly for HTTP.
What’s the difference between Vercel’s after() and waitUntil()?
after() is a higher-level API introduced in Next.js 15.1 that wraps waitUntil() under the hood. Vercel recommends after() for Next.js applications because it integrates more cleanly with the framework’s rendering lifecycle. Functionally, both extend the invocation’s lifetime so you can do post-response work like flushing metrics.
Is some metric data loss inevitable in serverless?
In extreme cases, yes. If the platform itself has an outage, or if your flush exceeds the time budget and gets killed, data can be lost. Cloudflare disclosed losing about 55% of customer logs during one 3.5-hour incident. The goal is to minimize loss through correct use of flush primitives, conservative batch sizes, and appropriate timeouts. Perfect completeness is not guaranteed by any system, but proper instrumentation gets you close.
If you’re ready to stop worrying about flush mechanics and start seeing metrics, Distlang Metrics handles the buffering and flushing patterns described in this article out of the box, with built-in support for Cloudflare Workers and Vercel.