Using after() in Vercel Edge Functions for Metrics Flush
The Problem: Metrics Vanish When Serverless Functions Die
Serverless functions are stateless and short-lived. Unlike a traditional server that stays running between requests, a serverless function spins up on demand, handles a request, and shuts down. Any buffered data that hasn’t been sent over the network by the time the runtime terminates simply disappears.
This creates a specific problem for metrics. Most metrics clients batch data for efficiency, collecting multiple measurements and sending them in a single HTTP request. Batching makes sense on a long-running server where you know the process will still be alive in a few seconds. In a serverless handler, that assumption breaks. As the Langfuse documentation puts it, “the serverless function might be frozen or terminated before the batch is flushed.”
The OneUptime team found the same issue when instrumenting Vercel functions with OpenTelemetry: “A batch processor might not flush before the function terminates, causing you to lose telemetry.” Their recommendation was to switch to a simple processor that sends each span immediately, trading network overhead for delivery guarantees.
The core tension is straightforward. You want to batch metrics for efficiency, but you need to flush them before the function dies. And you definitely don’t want the user waiting around while you do it.
Distlang Metrics is built specifically for this pattern, offering a lightweight JavaScript client designed for short-lived serverless handlers.
What after() Actually Does
after() is a function imported from next/server that schedules a callback to execute after the response (or prerender) has finished. According to the Next.js documentation, it is “useful for tasks and other side effects that should not block the response, such as logging and analytics.”
It became a stable API in Next.js 15. You can use it in Server Components, Server Functions, Route Handlers, and Proxy configurations.
Here’s the basic shape:
import { after } from 'next/server';
export async function GET(request) {
// Record your metrics during the request
metrics.counter('api.requests', 1, { route: '/api/data' });
// Schedule the flush to happen after the response
after(() => metrics.flush());
// Return immediately — the user doesn't wait for flush
return Response.json({ data: 'hello' });
}
The response goes out to the user right away. The runtime stays alive long enough for the after() callback to complete. This is the pattern for using after() in Vercel edge functions for metrics flush, and it works because Vercel extends the function’s execution lifetime to accommodate the deferred work.
after() vs waitUntil(): When to Use Each
These two APIs solve the same problem but at different levels of abstraction. Understanding the distinction matters because picking the wrong one can lead to subtle bugs or unnecessary complexity.
after() is the higher-level option. It ships with Next.js, understands the rendering lifecycle, and gives you access to request APIs like cookies and headers inside the callback (when used in Route Handlers or Server Functions). It is imported from next/server.
waitUntil() is the lower-level primitive. It comes from the @vercel/functions package and works with any framework deployed on Vercel, not just Next.js. It accepts a promise and extends the function’s lifetime until that promise settles.
Here’s the important detail: after() uses waitUntil() internally. As Vercel’s documentation explains, “this is achieved using a primitive called waitUntil(promise), which extends the lifetime of a serverless invocation until all promises passed to waitUntil have settled.” So after() is an abstraction on top of waitUntil(), not a replacement for it.
| Criterion | after() | waitUntil() |
|---|---|---|
| Import | next/server |
@vercel/functions |
| Requires | Next.js 15.1+ | Any framework on Vercel |
| Lifecycle awareness | Yes, understands Next.js render phases | No, raw promise extension |
| Request API access | Yes, in Route Handlers and Server Functions | Not directly |
| Runtime support | Node.js and Edge | Node.js and Edge |
Vercel’s own recommendation is clear: “If you’re using Next.js 15.1 or above, use after() from next/server instead.” If you’re on Remix, SvelteKit, or another framework, waitUntil() is your tool.
Practitioners on the Statsig community forums confirmed waitUntil(client.flush()) as the standard pattern before after() existed. One Statsig engineer noted: “Vercel may have a method that will keep the process running but allow you to call flush async without blocking the response.” That method is now after().
For a deeper look at how flush behavior interacts with batching, see configuring buffering and flush in the JavaScript client.
The Edge Runtime Confusion
The search phrase “Vercel edge functions” requires some clarification because the terminology has shifted significantly in 2025.
Standalone Edge Functions are deprecated. Vercel announced that Edge Functions now run on the unified Vercel Functions infrastructure. The Edge runtime still exists, but only as a runtime option you can select for a Vercel Function, not as a separate deployment target. Routing Middleware continues to use the Edge runtime by default, and that’s expected.
Lee Robinson, Vercel’s VP of Product, confirmed that Vercel “reverted all edge rendering back to Node.js,” making the Node.js runtime the default for most workloads. The practical implication: when developers talk about using after() in Vercel edge functions for metrics flush, they’re usually talking about Vercel Functions running on the Node.js runtime, with the Edge runtime reserved for middleware.
This matters for metrics instrumentation because the Edge runtime has a significant limitation: custom OpenTelemetry spans are not supported on it. If you need custom traces, you must use the Node.js runtime. For lightweight, code-first metrics that don’t depend on OTel, a simple JavaScript metrics client works across both runtimes.
Practical Pattern: Flushing Metrics with after()
Here’s a complete pattern showing how to instrument a Next.js Route Handler with metrics and flush them using after():
import { after } from 'next/server';
import { MetricSet } from '@distlang/client';
const metrics = new MetricSet({
name: 'api-metrics',
token: process.env.METRICS_TOKEN,
});
export async function GET(request) {
const start = Date.now();
// Your actual handler logic
const data = await fetchSomeData();
// Record metrics during the request
const duration = Date.now() - start;
metrics.histogram('api.latency_ms', duration, { route: '/api/data' });
metrics.counter('api.requests', 1, { status: '200' });
// Flush after the response, not during it
after(() => metrics.flush());
return Response.json(data);
}
The key points:
- Metrics are recorded synchronously during the request. This adds negligible overhead.
after()schedules the flush to happen afterResponse.json(data)is sent.- The user gets the response without waiting for the metrics HTTP request to complete.
- The Vercel runtime stays alive until
metrics.flush()resolves.
If you’re coming from Cloudflare Workers, the equivalent pattern uses ctx.waitUntil(). The concept is identical, just different APIs. You can see the Cloudflare-specific approach in the guide on instrumenting Workers with counters and histograms.
For a full walkthrough from instrumentation to dashboard, check out building from app to dashboard in minutes.
Three Flush Strategies, Ranked
Not all flush approaches are equal. Here they are from worst to best for serverless environments.
Fire-and-forget (unreliable). You call fetch() to send metrics without awaiting it and without wrapping it in after() or waitUntil(). As one DEV Community article noted, “in serverless environments, this can be unreliable as the function will stop computation after the response is sent.” Vercel’s own docs are blunt: “Any work not wrapped in waitUntil or after will be lost on cancellation.”
Blocking flush (reliable but slow). You await flush() before returning the response. This guarantees delivery but the user pays for it. Every millisecond your metrics HTTP request takes gets added to the response time.
Deferred flush via after() (recommended). The response returns immediately. The flush happens in the extended execution window. The user doesn’t wait. The data arrives. This is the pattern to use.
For more on buffering and flush strategies in short-lived handlers, including when batch mode is safe and when it’s not, that guide covers the tradeoffs in detail.
Gotchas and Limitations
Using after() in Vercel functions for metrics flush is not bulletproof. Several constraints can still cause data loss if you’re not careful.
The timeout is shared. Promises scheduled via after() or waitUntil() share the same timeout as the parent function. If your function has a 10-second limit and the request itself takes 9.5 seconds, your flush only gets 500ms. Vercel’s docs confirm: “Promises passed to waitUntil() will have the same timeout as the function itself.”
The 500ms SIGTERM window. When Vercel decides to terminate a function instance, your code gets a SIGTERM signal and exactly 500 milliseconds to finish. After that, the instance is killed. If your flush takes longer, the data is gone.
No success guarantee. The Inngest team’s analysis of waitUntil highlights a critical point: “it does not ensure success. For example, if the promise passed to waitUntil fails, the serverless function will still complete and return a response.” A network error during flush means lost metrics, and neither after() nor waitUntil() will retry for you.
after() runs even on errors. This is actually a feature. If your handler throws an error or returns a redirect, the after() callback still executes. For metrics, this is exactly what you want, because you want to record error rates too.
Batch processors are risky. If you’re using OpenTelemetry’s BatchSpanProcessor on a serverless function, it may accumulate spans and try to flush them on a timer. That timer might never fire before the function terminates. Prefer per-request flushing or a SimpleSpanProcessor in serverless contexts.
To understand the nuances of batch sizes and payload limits, see the guide on batching, payload format, and size limits for ingestion.
Comparison with Cloudflare Workers
Developers moving between Vercel and Cloudflare often find the patterns familiar but the APIs different. On Cloudflare Workers, the equivalent of after() is ctx.waitUntil():
// Cloudflare Workers pattern
export default {
async fetch(request, env, ctx) {
metrics.counter('requests', 1);
ctx.waitUntil(metrics.flush());
return new Response('ok');
}
};
A GitHub discussion on the Vercel community repo captured this confusion well. One developer asked: “There is waitUntil in Cloudflare Workers for that. How can I do this in Vercel Edge middleware?” The answer, as of 2025, is after() for Next.js apps and waitUntil() from @vercel/functions for everything else.
The Cloudflare Workers metrics quickstart covers the Worker-specific setup if you’re working across both platforms.
Getting Started with Vercel Metrics
If you’re building on Vercel and want to start flushing metrics properly with after(), the fastest path is:
- Install a lightweight metrics client that supports explicit flushing
- Record counters and histograms during your handler
- Call
after(() => client.flush())before returning - Check your dashboard
The Vercel metrics quickstart walks through this step by step, with copy-paste code for Next.js Route Handlers.
FAQ
Does after() work in the Edge runtime?
Yes. after() works in both the Node.js and Edge runtimes on Vercel. The deprecation of “Edge Functions” refers to standalone Edge Function deployments, not the Edge runtime itself. Routing Middleware still uses the Edge runtime, and after() functions correctly there.
What happens if the flush fails inside after()?
The response has already been sent to the user, so they won’t see an error. The promise simply rejects, and the metrics are lost for that request. Neither after() nor waitUntil() provide automatic retries. If reliable delivery is critical, you should add error handling and logging inside the callback.
Can I call after() multiple times in one request?
Yes. Each call schedules an additional callback. All of them will execute after the response is sent. This is useful if different parts of your code need to schedule independent cleanup work.
How long does my flush have to complete?
It shares the parent function’s timeout. If your Vercel Function has a 60-second limit and the request took 2 seconds, the flush gets up to 58 seconds. But if the function instance receives a SIGTERM signal, you have a hard ceiling of 500 milliseconds regardless.
Should I use after() or waitUntil() for metrics flush?
If you’re on Next.js 15.1 or later, use after(). It’s the recommended API, offers better DX, and understands the Next.js rendering lifecycle. If you’re using a different framework on Vercel (Remix, SvelteKit, Astro), use waitUntil() from @vercel/functions.
Is fire-and-forget ever acceptable for metrics?
On a long-running server, yes. In serverless, no. An unawaited fetch() that isn’t wrapped in after() or waitUntil() will likely be killed before it completes. Vercel explicitly warns that any work not wrapped in these APIs will be lost on cancellation.
Do I need OpenTelemetry for metrics on Vercel?
No. OpenTelemetry is one option, but it comes with constraints. Custom spans aren’t supported on the Edge runtime, and the BatchSpanProcessor can lose data in serverless contexts. A lightweight, code-first metrics API that supports explicit flushing is often simpler and more reliable for basic counters and histograms.
What’s the difference between Vercel Functions and Edge Functions in 2025?
Edge Functions as a separate concept are deprecated. Everything is now a “Vercel Function” that can run on either the Node.js runtime or the Edge runtime. Node.js is the default. The Edge runtime is primarily used for Routing Middleware. When people say “edge functions” today, they usually mean Vercel Functions running on one of these two runtimes.