Handle Short-Lived Handler Metrics in Workers: 2026 Guide



Serverless functions are designed to be stateless and short-lived. Unlike traditional servers, they are not kept alive between requests. This architecture is great for scaling and cost, but it creates a real problem for anyone trying to collect application metrics.

If you’ve ever deployed a Cloudflare Worker, a Vercel Edge Function, or a Next.js Route Handler and noticed gaps in your telemetry data, you’ve already encountered this issue. The handler finishes, the runtime freezes or discards the isolate, and your carefully buffered metrics vanish into nothing.

This guide explains exactly why metrics get lost in short-lived handlers and walks through the three main strategies to solve the problem.

If you want to skip straight to implementation, Distlang Metrics is built specifically for this use case, with a lightweight JS client designed for serverless flush patterns.

What Is a Short-Lived Handler?

A “handler” in this context is an entry point function that processes an incoming event. In Cloudflare Workers, that’s typically the fetch() handler that receives an HTTP request and returns a response. In Next.js, it might be a Route Handler or API route. In AWS Lambda, it’s your exported handler function.

These handlers are “short-lived” because the serverless runtime can freeze or terminate the execution environment immediately after the response is sent. Execution durations range from milliseconds to a few minutes at most. There is no persistent process running between requests.

The “metrics” part is where things get complicated. Most telemetry clients, whether for OpenTelemetry, Prometheus push, or custom analytics, assume they’re running inside a long-lived process. They batch data points in memory and flush them on a timer or when the process exits. Neither mechanism works when your code runs for 12 milliseconds and then ceases to exist.

Why Metrics Get Lost in Short-Lived Handlers

Understanding the failure modes is essential before picking a strategy to handle short-lived handler metrics in workers.

No persistent process

Traditional metrics libraries accumulate data points and ship them in batches, typically every 10 or 30 seconds. In a serverless handler that completes in under 100ms, that interval never fires. The data sits in memory. Then the isolate is discarded, and the data goes with it.

PostHog documents this directly in their Cloudflare Workers integration: they set flushAt to 1 and flushInterval to 0 because batched data is sent asynchronously and Workers can terminate before it’s sent, causing data loss.

Isolate freezing

Edge runtimes like Cloudflare Workers, Vercel Edge, and Deno Deploy freeze the isolate between invocations. There is no process.beforeExit, no SIGTERM, no reliable lifecycle hook. Your setInterval callback will never fire between requests because the entire execution context is frozen.

For a deeper look at this behavior, see our guide on buffering and flush strategies for short-lived handlers.

Client disconnect cancels background work

On Cloudflare Workers, when a client disconnects, outstanding tasks get canceled. If your metrics flush was running as a background promise and the client dropped the connection, your data is gone. The waitUntil() API provides a 30-second extension after disconnect, but only if you use it explicitly.

Silent failures in background work

Here’s a subtle one that most guides skip. A waitUntil() promise can fail after the response was already sent. The client sees a 200 OK. Your system recorded a failure. Data inconsistency results, and it’s difficult to diagnose because no error surfaced to the user. This orphaned background work risk is real and poorly documented.

Strategy 1: Flush-on-Exit with Lifecycle Hooks

The most common approach to handle short-lived handler metrics in workers is to explicitly flush your metrics client after the response is sent but before the isolate shuts down.

Cloudflare Workers: ctx.waitUntil()

The ctx.waitUntil() method keeps the isolate alive until a given promise resolves, without blocking the response to the client. The user sees the response immediately; the flush finishes in the background.

export default {
  async fetch(request, env, ctx) {
    // Handle request, record metrics
    const response = new Response("OK");

    // Flush metrics after response, before isolate dies
    ctx.waitUntil(metricsClient.flush());

    return response;
  }
};

Important caveat: waitUntil() doesn’t extend the timeout. It lets you do cleanup work after sending the response, but the isolate still shuts down at the same time limit. Practitioners on the Cloudflare Community forums have confirmed this behavior repeatedly.

For a detailed walkthrough of this pattern, read our post on waitUntil semantics in Cloudflare Workers.

Vercel / Next.js: after()

Starting with Next.js 15.1, Vercel recommends using the built-in after() function from next/server instead of waitUntil(). It schedules work that runs after the response has been sent, making it a natural fit for metrics flushing.

import { after } from 'next/server';

export async function GET(request) {
  // Record metrics during handling

  after(async () => {
    await metricsClient.flush();
  });

  return new Response("OK");
}

One practitioner found that with @vercel/otel in Next.js Route Handlers, custom OTEL metrics weren’t being properly reported because the handlers completed execution before the metrics had a chance to flush. The after() function was introduced partly to solve this exact timing problem.

We cover the Vercel-specific integration in detail in our guide on using after() for metrics flush.

Limits and risks of lifecycle hooks

Both waitUntil() and after() share the same fundamental limitation: they rely on the runtime granting you extra execution time. That time is finite. On Cloudflare Workers, you get roughly 30 seconds after a client disconnect. On Vercel, it depends on the function runtime configuration.

The bigger risk is silent failure. If your flush promise rejects inside waitUntil(), no one sees the error. Your client got a successful response. Your monitoring dashboard just has a gap. You need error handling and ideally some form of fallback logging inside these hooks.

Strategy 2: Fire-and-Forget Platform Bindings

Cloudflare offers an alternative that sidesteps the flush problem entirely: Analytics Engine.

When you call writeDataPoint() on an Analytics Engine binding, the write is non-blocking and does not impact request latency. It’s fire-and-forget from the Worker’s perspective. No await, no acknowledgement. The platform handles delivery internally.

export default {
  async fetch(request, env) {
    env.METRICS.writeDataPoint({
      blobs: ["GET", "/api/users"],
      doubles: [request.cf.tlsVersion ? 1 : 0],
      indexes: ["request"],
    });

    return new Response("OK");
  }
};

This is appealing because you don’t need waitUntil() at all. The data point is handed off to the platform immediately during the synchronous execution of your handler.

The tradeoffs are significant though. Analytics Engine is Cloudflare-specific, so you’re locked to one platform. The schema is limited to blobs and doubles. Retention is capped at 90 days. And you can’t send this data to your own backend or third-party tools without additional work.

For many teams, the built-in Cloudflare metrics (request counts, error rates, CPU time, wall time, execution duration) are enough. But if you need custom application-level counters and histograms, say latency distributions per endpoint or error counts by type, you’ll outgrow Analytics Engine quickly.

Our guide on counters and histograms in JavaScript covers the metric types that matter most in serverless.

Strategy 3: API-First Serverless Metrics Services

The third approach is to send metrics over HTTP to a dedicated metrics API. Instead of running agents, sidecars, or relying on platform-specific bindings, you use a lightweight client library that buffers data points in memory during the handler’s execution and flushes them via an HTTP POST inside waitUntil() or after().

This combines the best of both worlds. You get custom metric types (counters, histograms, whatever you need) without platform lock-in, and the flush pattern integrates naturally with the lifecycle hooks described above.

The workflow looks like this:

  1. Initialize the metrics client at module scope (reused across invocations if the isolate persists).
  2. Record metric data points during request handling.
  3. Call flush() inside waitUntil() or after() before the handler exits.
  4. The client sends a single batched HTTP request to the metrics API.

Distlang Metrics is purpose-built for this pattern. The JS client is lightweight, designed for serverless runtimes, and includes built-in buffering with explicit flush control. There are copy-paste quickstarts for both Cloudflare Workers and Vercel. No agents, no Prometheus infrastructure, no Grafana setup. You get a hosted dashboard per metric set that auto-generates charts from your incoming data.

For teams that need something portable across Cloudflare, Vercel, and other runtimes, this approach avoids the vendor lock-in of Analytics Engine while still solving the flush timing problem that plagues generic telemetry libraries.

Metric Types That Matter in Workers

When you handle short-lived handler metrics in workers, two metric types cover the vast majority of use cases.

Counters track things that only go up: total requests, error counts, cache hits. They’re the simplest metric type and the cheapest to record. One data point per event.

Histograms capture distributions: response latency, payload sizes, queue depths. They’re more expensive to compute but give you percentiles (p50, p95, p99) that raw averages hide.

Workers-specific built-in metrics typically include request counts, error rates, CPU time, wall time, and execution duration. These are useful for infrastructure monitoring. Custom application metrics (like “how many users hit the /checkout endpoint” or “what’s the p99 latency of my database query”) require instrumentation in your handler code.

For a broader look at metric types and best practices, see our metrics for developers guide.

Buffering and Batching in Ephemeral Handlers

Buffering strategy is critical when you handle short-lived handler metrics in workers. Too little buffering (flush every single data point) means you pay the latency cost of an HTTP request on every write. Too much buffering means data loss if the isolate is discarded before the batch ships.

The sweet spot for most serverless handlers: buffer data points in memory during the request, then flush the entire batch once at the end inside waitUntil() or after(). One HTTP request per invocation.

Some developers try to get clever with isolate reuse. One practitioner on the Cloudflare Community forum described a “TorchHolder” pattern using event.waitUntil() on every incoming request with a local expiry timeout of 25 seconds, batching data across requests within the same isolate. This exploits the fact that isolates are sometimes reused across requests.

The problem: isolate reuse is not guaranteed. Cloudflare makes no promises about when an isolate will be recycled. Building your metrics pipeline on an assumption the platform can invalidate at any time is fragile. The safe approach is to treat every invocation as potentially the last and flush accordingly.

For configuration details on batch sizes, flush intervals, and payload limits, our guide on configuring buffering and flush behavior covers the specifics.

Cross-Platform Comparison

How you handle short-lived handler metrics in workers varies by platform. Here’s a quick comparison of the three major serverless runtimes:

Feature Cloudflare Workers Vercel / Next.js AWS Lambda
Post-response hook ctx.waitUntil() after() (15.1+) or waitUntil() Lambda Extensions
Fire-and-forget binding Analytics Engine None built-in None built-in
Process exit signal None None (edge runtime) SIGTERM in some cases
Isolate reuse Possible, not guaranteed Possible, not guaranteed Container reuse common
gRPC support No (HTTP-only fetch) No (edge runtime) Yes (Node.js runtime)
Max post-response time ~30s after disconnect Varies by plan Up to 2 hours (extensions)

AWS Lambda is the most forgiving here because Lambda Extensions can run as a separate process that outlives the handler invocation. Amazon actually patented an “intermediate batch service for serverless computing environment metrics” to solve this at the platform level. But if you’re on Cloudflare or Vercel, you’re working with tighter constraints.

Common Mistakes

When developers first try to handle short-lived handler metrics in workers, they hit the same pitfalls repeatedly.

Relying on setInterval for periodic flushes. Timers freeze between invocations in edge runtimes. Your 10-second flush interval will never fire if no request arrives for 30 seconds.

Forgetting to flush entirely. If you initialize a metrics client and record data points but never call flush(), the data accumulates in memory and vanishes when the isolate is recycled. This is the most common cause of missing metrics.

Buffering too much data. Cloudflare Workers have memory limits (128MB on the free plan). If you’re recording thousands of high-cardinality data points without flushing, you can hit the memory ceiling and crash the isolate.

Assuming isolate reuse is guaranteed. As mentioned above, some patterns depend on the same isolate handling multiple requests. This works sometimes. It also silently drops data when it doesn’t.

Using gRPC exporters in edge runtimes. Cloudflare Workers and Vercel Edge Functions support HTTP fetch but not raw TCP sockets. OTEL’s default gRPC exporter will not work. You need HTTP-based exporters or an API-first approach.

Not handling flush errors. If metricsClient.flush() throws inside waitUntil(), the error is swallowed. Wrap your flush in a try/catch and log failures somewhere you can see them, even if it’s just console.error to the Worker’s log stream.

For a troubleshooting-focused take, our post on debugging missing metrics walks through common ingestion failures step by step.

Key Terms Quick Reference

Term Definition
Handler Entry point function (fetch, scheduled, route handler) that processes an incoming event in a serverless runtime.
Isolate V8 execution sandbox in edge runtimes. May be reused across requests or discarded after a single invocation.
ctx.waitUntil() Cloudflare Workers method that keeps the isolate alive for a background promise without blocking the response.
after() Next.js 15.1+ function that schedules work to run after the response is sent. Preferred over waitUntil() in the Vercel ecosystem.
Flush Forcibly sending all buffered metrics data to a backend before the handler exits. Critical in short-lived handlers where automatic flushing won’t happen.
Fire-and-forget A write pattern that returns immediately without waiting for acknowledgement. Used by Cloudflare Analytics Engine.
Counter A metric that only increases. Used for request counts, errors, cache hits.
Histogram A metric that captures a distribution of values. Used for latency percentiles, payload sizes.

Getting Started

If you’re ready to handle short-lived handler metrics in workers without setting up Prometheus, Grafana, or any agent infrastructure, Distlang Metrics gives you a working dashboard in minutes. The JS client is designed for serverless flush patterns, with quickstarts for both Cloudflare Workers and Vercel.

FAQ

Why can’t I just use a standard metrics library in Cloudflare Workers?

Standard metrics libraries like the OpenTelemetry SDK assume a long-running process with reliable shutdown hooks. In Workers, there’s no process.beforeExit and no SIGTERM. The isolate can freeze or terminate between requests without warning, so buffered data never ships. You need to explicitly flush inside ctx.waitUntil() or use a platform binding that handles delivery for you.

What happens if ctx.waitUntil() fails silently?

If the promise you pass to waitUntil() rejects, the error is not surfaced to the client (they already received their response). Your metrics for that invocation are lost, and you won’t know unless you’ve added error handling inside the waitUntil callback. Always wrap flush calls in try/catch and log failures to your Worker’s console.

Should I use Cloudflare Analytics Engine or an external metrics API?

Analytics Engine is great for simple, low-overhead telemetry that doesn’t need to leave Cloudflare’s ecosystem. If you need custom metric types, longer retention, portability across platforms, or the ability to send data to your own dashboards, an API-first service is the better choice.

Does after() work the same as waitUntil() in Vercel?

They serve the same purpose (running code after the response is sent) but after() is the recommended approach starting with Next.js 15.1. It’s integrated into the Next.js framework rather than being a lower-level Vercel platform API. For new projects on Vercel, use after().

Can I batch metrics across multiple requests in the same isolate?

Technically yes, if the isolate is reused. Some developers accumulate data points across requests and flush periodically. But isolate reuse is not guaranteed on any edge platform, so you risk losing an entire batch if the isolate is discarded. The safer pattern is to flush at the end of every invocation.

What metric types should I track in Workers?

Start with counters for request volume and error rates, and histograms for response latency. These cover the most common observability needs. Cloudflare’s built-in metrics handle infrastructure-level telemetry (CPU time, wall time), so focus your custom instrumentation on application-level behavior that the platform can’t see.

Why doesn’t gRPC work in edge runtimes?

Edge runtimes like Cloudflare Workers and Vercel Edge Functions provide an HTTP-based fetch API but don’t support raw TCP sockets. gRPC requires HTTP/2 with bidirectional streaming over TCP, which isn’t available in these environments. Use HTTP-based exporters or REST APIs instead.

How do I know if my metrics are actually being delivered?

Check your metrics backend for gaps that correlate with deployment times or traffic spikes. If you see consistent undercounting, you likely have a flush timing issue. Adding a console.log inside your waitUntil callback confirming successful flush is a simple first step. Some metrics services also provide ingestion logs that show received vs. expected data points.