cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Work after the response: after()

Run logging, analytics and cleanup with after() once the response is sent: when it runs, what it can read, how it fails, and its limits on serverless.

By the end, you can
  • 01
    Move work out of the visitor's wayHand logging and analytics to after(), so the response goes out first, and say when the callback runs.
  • 02
    Read the request the right wayTell where cookies() and headers() work inside the callback, and read them first in a Server Component.
  • 03
    Know its limitsHandle errors in after() work, explain waitUntil and maxDuration on serverless platforms, and pick a job queue for long or retried work.

An order that answers before the bookkeeping

Asha orders Mint tea in the tea shop. Two things have to happen: the shop saves her order and tells her it's placed, and the shop counts the order for its sales report. Asha cares about the first. The second is bookkeeping: she shouldn't wait for it.

Next.js has a function for exactly this split: after(), from next/server. You give it a function, called the callback, and Next.js runs it once the response has gone out. Here is the shop's order endpoint, a Route Handler, with one after(). This is a recording of a real Next.js 16.4.3 project: it was built with next build, run with next start, and used in real Chrome.

Mint tea's page, its order endpoint, and a logNext.js
app/_lib/activity/api/orders/
app/api/orders/route.ts
import { after } from "next/server";import { logLine } from "../../_lib/activity"; let nextId = 7; export async function POST(request: Request) {  const { tea } = await request.json();  const order = { id: nextId++, tea };  logLine(`Order ${order.id} saved: ${tea}`);   after(() => {    logLine(`after(): order ${order.id} counted for the sales report`);  });   logLine(`Order ${order.id}: answering now`);  return Response.json(order, { status: 201 });}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (5/5)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ◐ /activity└ ƒ /api/orders ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed contentƒ  (Dynamic)            server-rendered on demand $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ ReadyOrder 7 saved: Mint teaOrder 7: answering nowafter(): order 7 counted for the sales report

app/api/orders/route.ts answers POST /api/orders. Line 7 reads the order from the request's body, and line 8 gives it the id 7. Line 9 calls logLine from app/_lib/activity.ts, a stand-in for the shop's log service: it prints the line and adds it to a file. Then:

  • Lines 11 to 13 call after() with an arrow function. Nothing inside it runs yet: Next.js keeps the function for later.
  • Line 15 logs Order 7: answering now, and line 16 answers 201 with the order as JSON.
  • Once that answer has finished, Next.js calls the function, and line 12 writes the report line.

Open the Terminal tab: after the build's output, it shows what next start printed. The three lines come in that order: Order 7 saved: Mint tea, Order 7: answering now, and last after(): order 7 counted for the sales report, even though line 12 sits above line 15 in the file. Then open the Steps tab. Asha opened /, clicked Place order, and the page showed Order 7 placed from the 201. The last step opened /activity?lines=3, a page that shows the log once it has three lines (it waits at most 5 seconds, since the callback runs a moment after the answer).

A timeline from left to right. Inside the Route Handler: Order 7 saved, then after keeps the function, then answering now. Then 201 is sent to Asha. A dashed line marks the response finishing. After it, Next.js runs the function, which writes the report line. Asha waits for the first four parts only.your Route HandlerOrder 7 savedafter(fn)answering now201 sentfn is kept, not runNext.js, laterfn(): reportAsha waits for these fournobody waitsresponse finishedOrder 7 saved · answering now · after(): counted
The order's timeline, from the recording. after() only keeps the function; the handler goes on, and the 201 goes out. Once the response has finished, Next.js calls the function, and the report line is written. Asha waits for everything left of the dashed line, and for nothing after it.
Real-life analogyA tea counter and its sales book

At a busy tea counter, the server doesn't make Asha watch while the sale is written in the sales book. She gets her tea first. The server leaves a note on the counter, and writes the sale in the book once she has walked away with her cup.

In real life: Asha asks for a Mint tea
In Next.js: POST /api/orders arrives
In real life: The server makes the tea and hands it over
In Next.js: The handler saves the order and returns the 201
In real life: A note on the counter: write this sale in the book
In Next.js: after(() => …): a function kept for later
In real life: The server writes in the book once Asha has her tea
In Next.js: Next.js calls the function once the response has finished
The main idea

after(callback) schedules work for when the response has finished. The visitor gets the answer without waiting for that work. Use it for work whose result the visitor doesn't need: logging, analytics, small cleanups. It works in Server Components, Server Actions, Route Handlers and proxy.ts.

Before the answer, or after it

Without after(), the report line would be one more line in the handler. Everything a handler does before its return is something the visitor waits for, because the answer can't leave until the function returns it. Here are both versions of app/api/orders/route.ts, each built and used in Chrome the same way. Before you pick one, guess where the report line lands in the log.

Playground: where does the report line go in the log?
The tea shop, with this version of app/api/orders/route.tsNext.js
app/_lib/activity/api/orders/
app/api/orders/route.ts
import { logLine } from "../../_lib/activity"; let nextId = 7; export async function POST(request: Request) {  const { tea } = await request.json();  const order = { id: nextId++, tea };  logLine(`Order ${order.id} saved: ${tea}`);   logLine(`order ${order.id} counted for the sales report`);   logLine(`Order ${order.id}: answering now`);  return Response.json(order, { status: 201 });}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/activity?lines=3Tea shop
Activity log
  1. Order 7 saved: Mint tea
  2. order 7 counted for the sales report
  3. Order 7: answering now
Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. GET/activity?lines=3200a whole page
Try it yourself
Before the answer

Line 10 writes the report line inside the handler, before line 13 answers. The log reads: saved, counted, answering now.

Both versions were built with next build from Next.js 16.4.3 and used in real Chrome: open the page, click Place order, open the log. Steps shows the log page; Terminal shows what next start printed. Reset goes back to the first version.

Both logs have the same three lines. Before the answer, the report line sits in the middle: it ran inside the handler, so the 201 waited for it. In after(), it comes last: the 201 went out first. Writing a line to a file is quick, so here the difference is small. The same split matters when the bookkeeping is a call to another company's service that takes a second, or that fails. Step through both versions below. Before you start, guess: in which version does the report work stand between Asha's click and her answer?

Playground: who does what when Asha places an order
Who does whatBefore the answer
What the visitor seesafter step 1
tea-shop.example/

Mint tea

Place order

React is running: the page responds to clicks.

The page isn't complete yet.

Step 1 of 5
Browser: Asha clicks Place order

OrderButton sends POST /api/orders with {"tea":"Mint tea"}.

Each step is one thing that happens, in order. The screens are the pages recorded in real Chrome for the two versions above. Times aren't to scale. Reset goes back to the first version's first step.

In both versions, Asha sees Order 7 placed in step 5, once the answer arrives. Before the answer, step 3 is the report work itself, so it stands between her click and her answer. With after(), step 3 only hands the function over, and the report work is the last step: Next.js runs it once the response has finished, while the page already shows the answer.

When the after() work runs

"After the response" is a strong claim, so test it. In this version, the callback waits for a signal before it writes its line. The signal is a file, signal.txt, and only a later request writes it: POST /api/signal. If the response waited for the callback, the order would never get an answer.

An experiment: a callback that waits for a later requestNext.js
app/_lib/activity/api/orders/signal/
app/api/orders/route.ts
import { after } from "next/server";import { logLine } from "../../_lib/activity";import { signalSent } from "../../_lib/signal"; let nextId = 7; export async function POST(request: Request) {  const { tea } = await request.json();  const order = { id: nextId++, tea };  logLine(`Order ${order.id} saved: ${tea}`);   after(async () => {    await signalSent();    logLine(`after(): order ${order.id} counted for the sales report`);  });   logLine(`Order ${order.id}: answering now`);  return Response.json(order, { status: 201 });}
Recorded from Next.js 16.4.3

POST /api/orders201

Sent with the request
content-type application/json

The body sent with the request

{"tea":"Mint tea"}
Response headers
content-type application/json
vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch

The body that arrived, as it arrived

{"id":7,"tea":"Mint tea"}

Line 13 waits until signal.txt exists, checking every 50 milliseconds (app/_lib/signal.ts). Open the Network tab and go through the four requests, in the order the recording sent them:

  1. POST /api/orders answered 201 with {"id":7,"tea":"Mint tea"}, while its callback was still waiting.
  2. /activity?lines=2 shows two lines: saved, and answering now. The report line isn't there: the callback is still waiting.
  3. POST /api/signal writes the file and answers 204, which means "done, with nothing to send back".
  4. /activity?lines=3 now shows the third line, after(): order 7 counted for the sales report.

So the answer never waits for the callback. In Next.js's code, each request's callbacks wait in a queue that is paused until the response closes. A callback that waits for a file is only an experiment; real callbacks do their work and end.

A build-time render, a request-time render, a failed response

A callback runs once the work it belongs to has finished. That isn't always a visitor's request. Pick each version below and read its Terminal: the build's output comes first, then what next start printed while it answered two requests.

Playground: when does the after() work run?
The tea shop, with this versionNext.js
app/
app/page.tsx
import { after } from "next/server"; export default function Home() {  after(() => {    console.log("after(): the home page was rendered");  });  return (    <main>      <h1>Tea shop</h1>      <p>Fresh tea, sent every week.</p>    </main>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...after(): the home page was renderedafter(): the home page was rendered✓ Generating static pages (3/3)  Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○  (Static)  prerendered as static content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready
Try it yourself
A page made during the build

The home page reads nothing from the request, so next build prerenders it and saves it: ○ /. Its after() callback runs during that prerendering: this build printed its line twice, and no count is promised. The two visits get the saved page, and next start prints nothing for them.

Every version was built with next build from Next.js 16.4.3 and run with next start; the Terminal shows the build's output and, after it, what next start printed while it answered two requests. Reset goes back to the first version.
  • A page made during the build. The home page calls after() and reads nothing from the request, and the build still prints ○ /, prerendered as static content. after() isn't a request-time API, so calling it doesn't make a page render for each request. The build printed after(): the home page was rendered twice: in this build, Next.js rendered Home twice while it prerendered the page. Don't count on how many times. The two visits got the saved page, so Home didn't run, and neither did its callback.
  • A part made for each request. Greeting reads a cookie inside Suspense, so the route is ◐ /: a static shell, with Greeting made for each request (Partial Prerendering). Its callback runs after the visit's response: next start printed after(): home page shown to Asha on Mozilla/5.0 for Asha's visit.
  • A handler that fails. The handler schedules its callback on lines 4 to 6, then throws on line 7. The caller gets 500, and Next.js prints ⨯ Error: The payment service did not answer. The callback runs anyway: its line is in the Terminal. The docs say the same holds when a page calls notFound() or redirect().

What the callback can read

Logging usually wants something about the visitor: who they are (a cookie) and what they use (the user-agent header). cookies() and headers() from next/headers read them (Request-time APIs teaches both). Whether the callback can call them itself depends on where after() was called. Each version below asks for the same two values in a different place. Before you pick one, guess which version fails.

Playground: can this callback read the request?
The tea shop, with this versionNext.js
app/
app/page.tsx
import { Suspense } from "react";import { cookies, headers } from "next/headers";import { after } from "next/server"; async function Greeting() {  const name = (await cookies()).get("name")?.value ?? "guest";  after(async () => {    const agent = (await headers()).get("user-agent");    console.log(`after(): home page shown to ${name} on ${agent}`);  });  return <p>Welcome back, {name}.</p>;} export default function Home() {  return (    <main>      <h1>Tea shop</h1>      <Suspense fallback={<p>Loading…</p>}>        <Greeting />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (3/3)  Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ ReadyAn error occurred in a function passed to `after()`: Error: Route / used `headers()` inside `after()` while rendering. This is not supported. If you need this data inside an `after()` callback, use `headers()` outside of the callback. See more info here: https://nextjs.org/docs/app/api-reference/functions/after    at n (.next/server/chunks/ssr/_0aqpghaus1lkm._.js:30:14654)    at <unknown> (.next/server/chunks/ssr/_0aqpghaus1lkm._.js:1:211)An error occurred in a function passed to `after()`: Error: Route / used `headers()` inside `after()` while rendering. This is not supported. If you need this data inside an `after()` callback, use `headers()` outside of the callback. See more info here: https://nextjs.org/docs/app/api-reference/functions/after    at n (.next/server/chunks/ssr/_0aqpghaus1lkm._.js:30:14654)    at <unknown> (.next/server/chunks/ssr/_0aqpghaus1lkm._.js:1:211)
Try it yourself
Page: headers() inside

In a Server Component, after() runs once React has finished rendering, so Next.js refuses headers() inside the callback. The page still answers 200, but the callback stops with an error, and its line never prints.

Every version was built with next build from Next.js 16.4.3 and run with next start, with Asha's cookie and her browser's user-agent header. The Terminal shows what next start printed. Reset goes back to the first version.

Only the first version fails. Its Greeting is a Server Component, and line 8 calls headers() inside the callback. The page still answered 200 with Welcome back, Asha., but next start printed:

What next start printed for the first version (shortened)Text
An error occurred in a function passed to `after()`: Error: Route / used `headers()` inside `after()` while rendering. This is not supported. If you need this data inside an `after()` callback, use `headers()` outside of the callback.

In plain words: in a page, read the request while the component renders, not inside the callback. The reason is Partial Prerendering. Next.js finds out which parts of a page depend on the request by watching what each part reads while it renders. A callback runs after React has finished rendering, too late to be seen, so Next.js refuses request data there. The second version does it right:

app/page.tsx: read first, use laterTSX
import { Suspense } from "react";
import { cookies, headers } from "next/headers";
import { after } from "next/server";

async function Greeting() {
  const name = (await cookies()).get("name")?.value ?? "guest";
  const agent = (await headers()).get("user-agent");
  after(() => {
    console.log(`after(): home page shown to ${name} on ${agent}`);
  });
  return <p>Welcome back, {name}.</p>;
}

export default function Home() {
  return (
    <main>
      <h1>Tea shop</h1>
      <Suspense fallback={<p>Loading…</p>}>
        <Greeting />
      </Suspense>
    </main>
  );
}

Lines 6 and 7 read the cookie and the header while Greeting renders. The arrow function on lines 8 to 10 uses name and agent: it closes over them, so it still has them when it runs later. next start printed after(): home page shown to Asha on Mozilla/5.0.

Left, a window for Greeting while it renders: it reads name, Asha, from cookies, and agent, Mozilla/5.0, from headers. An arrow carries both values to the right, into a window for after the answer, where the callback uses name and agent. Below the right window, a crossed-out card: headers() called here, an error.while Greeting rendersrendername = "Asha"agent = "Mozilla/5.0"<p>Welcome back, Asha.</p>valuesafter the answercallbacklogs name and agentit reads nothing itselfheaders() here: errorLines 6 and 7 of the page
In a Server Component, read the request while the component renders. The callback closes over name and agent and uses them after React has finished. Calling headers() inside the callback is too late: Next.js refuses it, as the recorded error shows.

Route Handlers and Server Actions don't have this rule: the docs, and the check in Next.js's code, let them call request APIs everywhere, inside after() too. The third version's callback read the header itself and printed after(): Mint tea ordered on TeaApp/2.1. Each row is one place you can call after() from:

Reading the request in an after() callback
Where after() is calledCan the callback call cookies() and headers()?What to do
A page, a layout or generateMetadata (Server Components)No. The callback stops with an error, and the page still answers.Read cookies() and headers() while the component renders, and use the values in the callback.
A Route HandlerYes.Read them inside the callback, or before it.
A Server ActionYes, in the callback the action itself schedules.Read them inside the callback, or before it.
proxy.tsIt has the request itself.Read what you need from request before you call after().

after() in Server Actions and proxy.ts

A Server Action is where many writes happen, so it's a common place for after(). This order form posts to one. It's the version from the playground above:

An order form whose Server Action logs in after()Next.js
app/order/thanks/
app/order/actions.ts
"use server"; import { cookies, headers } from "next/headers";import { redirect } from "next/navigation";import { after } from "next/server"; export async function placeOrder(formData: FormData) {  const tea = String(formData.get("tea"));  console.log(`Order saved: ${tea}`);  after(async () => {    const name = (await cookies()).get("name")?.value ?? "guest";    const agent = (await headers()).get("user-agent");    console.log(`after(): ${tea} ordered by ${name} on ${agent}`);  });  redirect("/thanks");}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (4/4)  Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ○ /order└ ○ /thanks ○  (Static)  prerendered as static content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ ReadyOrder saved: Mint teaafter(): Mint tea ordered by Asha on Mozilla/5.0

The recording submitted the form the way a browser does before the page's JavaScript has loaded, with Asha's cookie. In app/order/actions.ts, line 9 logs the order (a stand-in for saving it; a real action would also check who is asking, as Authorization shows). Lines 10 to 14 schedule the callback, which reads cookies() and headers() itself. Line 15 calls redirect("/thanks"). In the Network tab, POST /order answered 303 with location: /thanks. In the Terminal, the callback still printed after(): Mint tea ordered by Asha on Mozilla/5.0, after Order saved: Mint tea.

after() also works in proxy.ts, which runs before every matched request. Logging each request there, after the response has gone, keeps the check in front of your routes quick:

proxy.ts logs each page it lets throughNext.js
app/teas/
proxy.ts
import { after, NextResponse, type NextRequest } from "next/server"; export function proxy(request: NextRequest) {  const path = request.nextUrl.pathname;  after(() => {    console.log(`after(): proxy.ts let ${path} through`);  });  return NextResponse.next();} export const config = { matcher: ["/((?!_next/).*)"] };
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (4/4)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ○ /teas ƒ Proxy (Middleware) ○  (Static)  prerendered as static content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Readyafter(): proxy.ts let / throughafter(): proxy.ts let /teas through

Line 4 reads the path from the request object while proxy.ts runs. Lines 5 to 7 hand after() a function that logs it. Line 8 lets the request through to its page. The recording asked for /, then /teas, then / again, and the Terminal shows after(): proxy.ts let / through and after(): proxy.ts let /teas through, each printed after its page had been sent.

Logging and analytics, and when they fail

Analytics usually means calling another company's service. That call can be slow, and it can fail. In after(), neither reaches the visitor. In this recording, the analytics service is a stand-in: a small server the recording starts in its place, whose address the route reads from ANALYTICS_URL. For order 7 it answers 503, "down for maintenance"; for order 8 it answers 200.

after() sends each order to the analytics serviceNext.js
app/_lib/activity/api/orders/
app/api/orders/route.ts
import { after } from "next/server";import { logLine } from "../../_lib/activity"; let nextId = 7; export async function POST(request: Request) {  const { tea } = await request.json();  const order = { id: nextId++, tea };  after(async () => {    const response = await fetch(`${process.env.ANALYTICS_URL}/events?order=${order.id}`);    if (!response.ok) throw new Error(`Analytics answered ${response.status}`);    logLine(`after(): order ${order.id} sent to analytics`);  });  return Response.json(order, { status: 201 });}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (4/4)  Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ◐ /activity└ ƒ /api/orders ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed contentƒ  (Dynamic)            server-rendered on demand $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ ReadyAn error occurred in a function passed to `after()`: Error: Analytics answered 503    at <unknown> (.next/server/chunks/[root-of-the-server]__0vw7ijbc6l9p6._.js:1:248)after(): order 8 sent to analytics

Line 10 calls the service with fetch. Line 11 throws when it answers with an error, and line 12 logs the order when it doesn't. In the Network tab, both orders answered 201, order 7 too. The stand-in received GET /events?order=7 and GET /events?order=8 while next start ran, so both callbacks ran. In the Terminal, Next.js printed An error occurred in a function passed to `after()`: Error: Analytics answered 503 for order 7, and after(): order 8 sent to analytics for order 8.

A failed callback is logged, and that's all

Next.js catches what a callback throws and prints it with console.error. The visitor never sees it, and nothing tries the work again, and a crash before the callback runs loses it too. So after() is only for events you can afford to lose. An event that must arrive is saved with the order, before the answer, and sent by a job queue.

A few more things the docs say about the function you pass:

  • You can wrap it. after() calls can be nested, so a helper like track(event) can call after() itself, and any route can call the helper.
  • It can reuse what the render loaded. In Server Components, a function wrapped in React's cache() returns the value the render already computed, so the callback doesn't load it again.

Limits on serverless platforms

With next start, as in every recording above, the server keeps running between requests, so it can finish a callback whenever the response has gone. The docs list after() as fully supported on a Node.js server and in Docker. When you stop such a server, send it SIGTERM or SIGINT and give it time: it finishes the requests in flight and runs the waiting callbacks before it exits. A static export has no server, so it has no after().

Many platforms run Next.js as serverless functions instead: a copy of your server code that the platform starts for requests and may freeze or stop once it has answered. Work still running after the answer would be cut off. So the platform gives Next.js a function called waitUntil(promise), which keeps the function running until the promise settles. after() passes its work to it. On Vercel it's built in. An adapter for another platform provides its own, in the place the docs describe:

Where Next.js looks for waitUntil (from the after() docs)JavaScript
const RequestContext = globalThis[Symbol.for("@next/request-context")];
const waitUntil = RequestContext?.get()?.waitUntil;

Line 1 reads a request context that the platform puts on globalThis, under a shared symbol. Line 2 takes its waitUntil. If there's none, Next.js throws when you call after(): `after()` will not work correctly, because `waitUntil` is not available in the current environment.

Two rows, each a serverless function's life from left to right. Top row, without waitUntil: request, answer, then the function may stop, and the after work is crossed out. Bottom row, with waitUntil: request, answer, after work, then stop. A label under both says maxDuration counts from the request.without waitUntilrequestanswerfunction stopsafter() workwith waitUntilrequestanswerafter() workstopsmaxDuration counts from the request, in both rows
A serverless function may stop once it has answered. Next.js passes the after() work to the platform's waitUntil, which keeps the function running until the work settles. Either way, the route's time limit (maxDuration, or the platform's default) counts from the request.

Even with waitUntil, the function has a time limit, and after() work counts toward it. The docs say the callback runs for the platform's default or configured maximum duration of the route. Platforms that let you set it read maxDuration, in seconds, from the route's file:

A 30-second limit for the orders route (from the docs, not recorded)TypeScript
// app/api/orders/route.ts
export const maxDuration = 30;

export async function POST(request: Request) {
  // …the order and its after() work, as above
}

Line 2 exports the limit. For Server Actions, the docs say to set it in the page that uses them. It's for hosting platforms, which read it from the build's output; each platform has its own default and maximum.

after() isn't a job queue

A job queue is a list of work saved outside your server, like in Redis or a database, with one or more workers: programs that take the next job and run it. Libraries like BullMQ, and hosted services like Inngest, Trigger.dev and Upstash QStash, work this way. Each row compares the two:

after() or a job queue
Questionafter()A job queue
When does the work start?Right after this response has finished.When a worker picks the job up, now or at a time you choose.
What if it fails?Next.js logs the error. Nothing tries again unless your code does.The queue tries again, as many times as you set.
What if the server stops?next start finishes waiting callbacks on SIGTERM. A crash loses them.The job is saved in the queue, so another worker can run it.
How long can it take?As long as the route is allowed to run: maxDuration, or the platform's default.As long as the worker allows, often minutes or hours.
Good forA log line, an analytics event, a small cleanup.Thousands of emails, a charge to try again tomorrow, a long report.

Use after() only for work you can afford to lose. Long work, work that must be retried, and work for later go in a queue, and a job that must happen is added before the request is confirmed, never in after(): if the server crashed between the answer and the callback, the job would be lost while Asha had already read Order 7 placed. Best of all, save the job in the same database transaction as the order (this is called a transactional outbox): either both are saved or neither is, and a worker sends the saved jobs to the queue.

Which work belongs in after()?

For each piece of work, ask: does the answer depend on it? Then it runs before the answer. Is it small, and fine to lose now and then? after(). Is it long, retried, or for later? A job queue. Sort the tea shop's jobs:

Where should this work run?
  • Write a log line that an order was placed.
  • Send the order to the analytics service.
  • Store when this customer last visited.
  • Check that Mint tea is in stock before confirming the order.
  • Save the order in the database.
  • Email all 4,000 customers about a new tea.
  • Try a failed charge again tomorrow.
  • Build a sales report that takes 20 minutes.
Try it yourself
0 of 8 correct

Sort each job: before the answer, in after(), or in a job queue.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "after() runs its function right away, in the background." No. It keeps the function, and Next.js calls it once the response has finished. The log shows answering now before the callback's line.
  • "The response waits for after() work to finish." No. In the experiment, the order answered 201 while its callback was still waiting for a later request.
  • "Calling after() makes a page render for each request." No. It isn't a request-time API: the page with only after() was ○ /, and its callback ran during the build.
  • "A page can read cookies() inside its after() callback." No. In Server Components the callback stops with an error. Read the values while the component renders, and use them in the callback.
  • "If the callback throws, the visitor gets an error." No. The answer has already gone. Next.js logs the error, as it did for order 7's failed analytics call.
  • "after() is a job queue." No. It runs once, right after this response, within the route's time limit, and never retries. Long or retried work belongs in a queue.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upRead the log's last line

Asha orders Mint tea once. Which line does the log show last?

Starter codeTypeScript
import { after } from "next/server";
import { logLine } from "../../_lib/activity";

let nextId = 7;

export async function POST(request: Request) {
  const { tea } = await request.json();
  const order = { id: nextId++, tea };
  logLine(`Order ${order.id} saved: ${tea}`);

  after(() => {
    logLine(`after(): order ${order.id} counted for the sales report`);
  });

  logLine(`Order ${order.id}: answering now`);
  return Response.json(order, { status: 201 });
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upDuring the build, or for each visit?

    This is app/page.tsx. When does line 5 print: during the build, or once for every visit?

    Starter codeTSX
    import { after } from "next/server";
    
    export default function Home() {
      after(() => {
        console.log("after(): the home page was rendered");
      });
      return (
        <main>
          <h1>Tea shop</h1>
          <p>Fresh tea, sent every week.</p>
        </main>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the page's callback

      Every visit to this page prints Route / used `headers()` inside `after()` while rendering. Which line has to move out of the callback?

      Starter codeTSX
      import { Suspense } from "react";
      import { cookies, headers } from "next/headers";
      import { after } from "next/server";
      
      async function Greeting() {
        const name = (await cookies()).get("name")?.value ?? "guest";
        after(async () => {
          const agent = (await headers()).get("user-agent");
          console.log(`after(): home page shown to ${name} on ${agent}`);
        });
        return <p>Welcome back, {name}.</p>;
      }
      
      export default function Home() {
        return (
          <main>
            <h1>Tea shop</h1>
            <Suspense fallback={<p>Loading…</p>}>
              <Greeting />
            </Suspense>
          </main>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeDon't let analytics fail an order

        This version calls the analytics service before it answers. For order 7, the service answers 503. What status does order 7 get? Then rewrite the route so a broken analytics service can't fail an order.

        Starter codeTypeScript
        import { logLine } from "../../_lib/activity";
        
        let nextId = 7;
        
        export async function POST(request: Request) {
          const { tea } = await request.json();
          const order = { id: nextId++, tea };
          const response = await fetch(`${process.env.ANALYTICS_URL}/events?order=${order.id}`);
          if (!response.ok) throw new Error(`Analytics answered ${response.status}`);
          logLine(`order ${order.id} sent to analytics`);
          return Response.json(order, { status: 201 });
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePredict a Route Handler's callback

          The shop's phone app sends POST /api/orders with {"tea":"Mint tea"} and the header user-agent: TeaApp/2.1. What does line 8 print?

          Starter codeTypeScript
          import { headers } from "next/headers";
          import { after } from "next/server";
          
          export async function POST(request: Request) {
            const { tea } = await request.json();
            after(async () => {
              const agent = (await headers()).get("user-agent");
              console.log(`after(): ${tea} ordered on ${agent}`);
            });
            return Response.json({ tea }, { status: 201 });
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · Challengeafter() or a job queue?

            The tea shop has three jobs to run in the background. Which of them fits after()?

            Starter codeText
            1. Write one line to the log when an order is placed.
            2. Send each of 4,000 customers an email about a new tea.
            3. Charge a card again tomorrow if today's charge fails.

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, picture the answer leaving first, then ask where the callback was scheduled.

              The after() quiz · 7 questionsScore: first tries count
              1. Question 1 of 7This handler logs three lines. In what order does next start print them?

                Read the code, then predictTypeScript
                import { after } from "next/server";
                import { logLine } from "../../_lib/activity";
                
                let nextId = 7;
                
                export async function POST(request: Request) {
                  const { tea } = await request.json();
                  const order = { id: nextId++, tea };
                  logLine(`Order ${order.id} saved: ${tea}`);
                
                  after(() => {
                    logLine(`after(): order ${order.id} counted for the sales report`);
                  });
                
                  logLine(`Order ${order.id}: answering now`);
                  return Response.json(order, { status: 201 });
                }
                

                Choose an answer to see the explanation.

              2. Question 2 of 7This page reads nothing from the request. When does its after() callback print?

                Read the code, then predictTSX
                import { after } from "next/server";
                
                export default function Home() {
                  after(() => {
                    console.log("after(): the home page was rendered");
                  });
                  return (
                    <main>
                      <h1>Tea shop</h1>
                      <p>Fresh tea, sent every week.</p>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 7A Server Component calls headers() inside its after() callback. What happens?

                Read the code, then predictTSX
                import { Suspense } from "react";
                import { cookies, headers } from "next/headers";
                import { after } from "next/server";
                
                async function Greeting() {
                  const name = (await cookies()).get("name")?.value ?? "guest";
                  after(async () => {
                    const agent = (await headers()).get("user-agent");
                    console.log(`after(): home page shown to ${name} on ${agent}`);
                  });
                  return <p>Welcome back, {name}.</p>;
                }
                
                export default function Home() {
                  return (
                    <main>
                      <h1>Tea shop</h1>
                      <Suspense fallback={<p>Loading…</p>}>
                        <Greeting />
                      </Suspense>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7In which of these can the after() callback itself call cookies()?

                Choose an answer to see the explanation.

              5. Question 5 of 7The after() callback throws because the analytics service is down. What does the visitor get?

                Choose an answer to see the explanation.

              6. Question 6 of 7Does calling after() make a route render for each request?

                Choose an answer to see the explanation.

              7. Question 7 of 7The shop must email 4,000 customers and try each failed email again later. Where should that work run?

                Choose an answer to see the explanation.

              Key takeaways

              • after(callback) from next/server keeps the function and runs it once the response has finished. The visitor never waits for it.
              • It works in Server Components, Server Actions, Route Handlers and proxy.ts. It isn't a request-time API: in a page made during the build, the callback runs during the build.
              • The callback runs even when the response failed: after an error, a redirect or a not-found page.
              • In a Server Component, read cookies() and headers() while it renders and use the values in the callback. Route Handlers and Server Actions can read them inside the callback.
              • A callback that throws is logged with console.error. The visitor keeps their answer, and nothing retries.
              • On serverless platforms, after() uses the platform's waitUntil, within the route's time limit (maxDuration). Long, retried or later work belongs in a job queue.

              Remember the one-liner.
              Answer first; after() does the bookkeeping once the response has gone.

              Read it in Next.js's own code

              These links point at release v16.4.3, the version every example on this page was built with. after.ts, lines 10 to 23, hands the callback to the request's after context. In after-context.ts, the callbacks wait in a queue that is paused when the request starts and started when the response closes, and a callback's error is printed with console.error. Which callbacks may call cookies() and headers() is decided in isRequestApiAllowedInCurrentPhase. The course's last stage, Under the hood, reads code like this.

              Coming next: Building AI features. A model's answer is slow and costs money for every word, so it's streamed to the page piece by piece, with the key, the costs and the input under control on the server.

              CompleteFrontend Clear concepts. Working examples.