after()
Send the response first, then finish the slow work.
What it is
after(callback) from next/server schedules work to run once the response has been sent. The visitor gets their answer straight away, and on Vercel the function stays alive until the callback finishes (up to the route's maximum duration). It works in Server Components, Server Actions, route handlers and Proxy, and runs even if the response failed.
When to use it
- Logging and analytics that shouldn't slow the page
- Sending emails or webhooks after a form is saved
- Warming or updating caches after a change
1. A Server Action with background work
The action answers at once. The work it hands to after() takes 2 seconds and writes to the log when done.
2. Finished work (recorded in Redis)
Loading the log…
Show code · src/app/after/actions.ts
"use server";
import { after } from "next/server";
import { logAfter } from "@/lib/after-log";
import { delay } from "@/lib/delay";
import { checkLimit, demoLimit, retryIn } from "@/lib/redis";
export type AfterState = { id: string; respondedAt: number } | { error: string } | null;
// Returns straight away. The slow part (2 seconds of "sending an email") is
// handed to after(): Vercel keeps the function alive until it finishes, but
// the visitor isn't kept waiting for it.
export async function startJob(): Promise<AfterState> {
const limit = await checkLimit(demoLimit);
if (limit && !limit.success) return { error: `Slow down: try again in ${retryIn(limit.reset)} s.` };
const id = crypto.randomUUID().slice(0, 8);
const respondedAt = Date.now();
after(async () => {
await delay(2000);
await logAfter({ id, respondedAt, finishedAt: Date.now() });
});
return { id, respondedAt };
}
Show code · src/app/after/AfterButton.tsx
"use client";
import { useRouter } from "next/navigation";
import { useActionState } from "react";
import { type AfterState, startJob } from "./actions";
type ButtonState = (AfterState & { tookMs?: number }) | null;
// Times the round trip in the browser, then refreshes the log once the
// background work should be done.
export function AfterButton() {
const router = useRouter();
const [state, action, pending] = useActionState<ButtonState>(async () => {
const sentAt = performance.now();
const result = await startJob();
if (result && "id" in result) setTimeout(() => router.refresh(), 2500);
return result && { ...result, tookMs: Math.round(performance.now() - sentAt) };
}, null);
return (
<form action={action}>
<button type="submit" disabled={pending}>{pending ? "Waiting for the response…" : "Start a job"}</button>
{state && "error" in state && <p className="error" role="status">{state.error}</p>}
{state && "id" in state && (
<p role="status">
Response for job <code>{state.id}</code> arrived after <strong>{state.tookMs} ms</strong>. The 2-second work is still
running on the server; the log below refreshes when it's done.
</p>
)}
</form>
);
}