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.
- 01Move work out of the visitor's wayHand logging and analytics to after(), so the response goes out first, and say when the callback runs.
- 02Read the request the right wayTell where cookies() and headers() work inside the callback, and read them first in a Server Component.
- 03Know 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.
app/api/orders/route.tsimport { 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 });}$ 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 reportapp/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 answers201with 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).
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.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/ordersarrives - 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
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.
app/api/orders/route.tsNext.jsapp/api/orders/route.tsimport { 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 });}Each step was taken in real Chrome against next start. Pick one to see the page after it.
- Order 7 saved: Mint tea
- order 7 counted for the sales report
- 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
GET/activity?lines=3200a whole page
Line 10 writes the report line inside the handler, before line 13 answers. The log reads: saved, counted, answering now.
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?
Before the answerafter step 1Mint tea
Place order
React is running: the page responds to clicks.
The page isn't complete yet.
OrderButton sends POST /api/orders with {"tea":"Mint tea"}.
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.
app/api/orders/route.tsimport { 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 });}POST /api/orders201
- Sent with the request
content-typeapplication/json
The body sent with the request
{"tea":"Mint tea"}- Response headers
content-typeapplication/jsonvaryrsc, 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:
POST /api/ordersanswered201with{"id":7,"tea":"Mint tea"}, while its callback was still waiting./activity?lines=2shows two lines: saved, and answering now. The report line isn't there: the callback is still waiting.POST /api/signalwrites the file and answers204, which means "done, with nothing to send back"./activity?lines=3now 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.
app/page.tsximport { 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> );}$ 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✓ ReadyThe 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.
- 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 printedafter(): the home page was renderedtwice: in this build, Next.js renderedHometwice while it prerendered the page. Don't count on how many times. The two visits got the saved page, soHomedidn't run, and neither did its callback. - A part made for each request.
Greetingreads a cookie insideSuspense, so the route is◐ /: a static shell, withGreetingmade for each request (Partial Prerendering). Its callback runs after the visit's response:next startprintedafter(): home page shown to Asha on Mozilla/5.0for 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 callsnotFound()orredirect().
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.
app/page.tsximport { 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> );}$ 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)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.
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:
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:
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.
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:
| Where after() is called | Can 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 Handler | Yes. | Read them inside the callback, or before it. |
| A Server Action | Yes, in the callback the action itself schedules. | Read them inside the callback, or before it. |
proxy.ts | It 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:
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");}$ 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.0The 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.tsimport { 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/).*)"] };$ 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 throughLine 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.
app/api/orders/route.tsimport { 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 });}$ 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 analyticsLine 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.
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 liketrack(event)can callafter()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:
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.
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:
// 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:
| Question | after() | 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 for | A 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:
- 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.
Sort each job: before the answer, in after(), or in a job queue.
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 nowbefore the callback's line. - "The response waits for after() work to finish." No. In the experiment, the order answered
201while 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.
Asha orders Mint tea once. Which line does the log show last?
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 });
}
after(): order 7 counted for the sales report. Line 12 is inside the callback, so it runs after line 15 and after the 201. The recording prints saved, answering now, then the report line.
This is app/page.tsx. When does line 5 print: during the build, or once for every visit?
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>
);
}
During the build. The page reads nothing from the request, so it's ○ /, rendered while next build prerenders it, and its callback runs then. The recorded build printed the line; the two visits printed nothing.
Every visit to this page prints Route / used `headers()` inside `after()` while rendering. Which line has to move out of the callback?
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>
);
}
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>
);
}
Line 8, const agent = (await headers()).get("user-agent");, moves above after(), so Greeting reads the header while it renders. The callback uses agent from its closure. The recorded fixed page printed after(): home page shown to Asha on Mozilla/5.0.
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.
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 });
}
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 });
}
500. The analytics call is awaited before the answer, so when it fails, line 9 throws and the whole order fails. The recording of this version answered 500 for order 7. Moving the call into after(), as in the analytics route, answers 201 for both orders and only logs the error.
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?
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 });
}
after(): Mint tea ordered on TeaApp/2.1. In a Route Handler, request APIs work inside after() too, so line 7 reads the header and line 8 prints it. The recording printed exactly this line.
The tea shop has three jobs to run in the background. Which of them fits after()?
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.Only job 1. A log line is small and fine to lose now and then. Job 2 is too long for one request's time limit, and each email may need a retry. Job 3 must run tomorrow and survive restarts. Both belong in a job queue.
Check your understanding
For each question, picture the answer leaving first, then ask where the callback was scheduled.
Question 1 of 7This handler logs three lines. In what order does
next startprint them?Read the code, then predictTypeScriptimport { 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.
Question 2 of 7This page reads nothing from the request. When does its
after()callback print?Read the code, then predictTSXimport { 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.
Question 3 of 7A Server Component calls
headers()inside itsafter()callback. What happens?Read the code, then predictTSXimport { 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.
Question 4 of 7In which of these can the
after()callback itself callcookies()?Choose an answer to see the explanation.
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.
Question 6 of 7Does calling
after()make a route render for each request?Choose an answer to see the explanation.
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)fromnext/serverkeeps 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()andheaders()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'swaitUntil, 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.
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.