after()

Schedules a callback to run once the response has finished streaming, without delaying the user.

Signature
function after(callback: () => void | Promise<void>): void
  • Added in Next.js v15.1.0

Parameters

ParameterTypeDescription
$callback() => void | Promise<void>A callback function which will be executed after the response (or prerender) is finished.

Return value

voidNothing. The callback is queued and executed after the response (or prerender) finishes.

Errors

Calling cookies(), headers() or another request-time API inside the callback throws a runtime error when after() is used in a Server Component, page, layout or generateMetadata. It is allowed inside Route Handlers and Server Functions.

Changelog

  • PHP v15.1.0after became stable.
  • PHP v15.0.0-rcunstable_after introduced.

after() queues work to run once the response has been sent. The user is not waiting on it, so it is where logging, analytics, audit trails, cache warming and webhook fan-out belong. It works in Server Components (including generateMetadata), Server Functions, Route Handlers and proxy.js.

What trips people up

  • Request APIs depend on where you call it. Inside a Route Handler or Server Function you may call cookies() and headers() in the callback. Inside a page, layout or Server Component you may not — after runs past React's render lifecycle, so Next.js cannot attribute the request access to a part of the tree. Read the values before after() and close over them.
  • It is not a request-time API, so it does not make a route dynamic. On a static page the callback fires at build time or on revalidation, not on every view.
  • The callback still runs when the request failed — including after a thrown error, notFound() or redirect(). Do not treat it as a success hook.
  • It runs under your route's max duration; a long job needs maxDuration raised or a real queue. Static export does not support it at all.

Example

Example · typescript
import { after } from 'next/server'
import { cookies, headers } from 'next/headers'
import { logUserAction } from '@/app/utils'

export async function POST(request: Request) {
  // Perform the mutation first
  const body = await request.json()
  await saveOrder(body)

  after(async () => {
    const userAgent = (await headers()).get('user-agent') || 'unknown'
    const sessionCookie =
      (await cookies()).get('session-id')?.value || 'anonymous'

    logUserAction({ sessionCookie, userAgent })
  })

  return Response.json({ status: 'success' })
}

When to use it

  • Log a user action to analytics after a mutation returns
  • Write an audit record after a Server Function succeeds
  • Send a webhook once a Route Handler has already replied 200
  • Warm a downstream cache after rendering, without slowing the page

Frequently asked questions

Why does cookies() throw inside my after() callback?

Because you are calling after() from a Server Component, page, layout or generateMetadata, where request-time APIs are not available in the callback — after runs past React's render lifecycle, so Next.js cannot attribute the access for Partial Prerendering. Read the value before after() and close over it. In Route Handlers and Server Functions the call is allowed.

Does after() make my route dynamic?

No. after is not a request-time API, so calling it never forces dynamic rendering. On a static page the callback runs at build time or whenever the page is revalidated, which surprises people expecting one analytics event per page view.

Does after() run if the request errors out?

Yes. The callback executes even when the response did not complete successfully, including when an error is thrown or notFound() or redirect() is called. Check for success explicitly inside the callback if you only want the side effect on the happy path.

More examples

In a Server Component, read request data first

The two reads happen during the component's render lifecycle, where Next.js can attribute them; the callback only closes over the resulting strings. Moving either await inside the after callback turns this into a runtime error — that restriction applies to pages, layouts, generateMetadata and any Server Component, but not to Route Handlers.

Example · typescript
import { after } from 'next/server'
import { cookies, headers } from 'next/headers'
import { logUserAction } from '@/app/utils'

export default async function Page() {
  // Read request data before `after` — this is allowed
  const userAgent = (await headers()).get('user-agent') || 'unknown'
  const sessionCookie =
    (await cookies()).get('session-id')?.value || 'anonymous'

  after(() => {
    // Use the values captured above
    logUserAction({ sessionCookie, userAgent })
  })

  return <h1>My Page</h1>
}

Combining after with a Suspense boundary

The heading and the fallback ship in the static shell while DynamicContent reads the cookie per request and hands the value to after through the closure. This is the pattern to copy under Cache Components: the request read stays inside the suspended subtree, and the post-response work stays outside the render.

Example · typescript
import { Suspense } from 'react'
import { after } from 'next/server'
import { cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'

export default function Page() {
  return (
    <>
      <h1>Part of the static shell</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <DynamicContent />
      </Suspense>
    </>
  )
}

async function DynamicContent() {
  const sessionCookie =
    (await cookies()).get('session-id')?.value || 'anonymous'

  after(() => {
    logUserAction({ sessionCookie })
  })

  return <p>Your session: {sessionCookie}</p>
}

Auditing a Server Function

The user's form submission returns as soon as the update and revalidation are done; the audit write and the webhook happen afterwards. Because this is a Server Function rather than a component, cookies() inside the callback is permitted. Both queued calls still count against the route's max duration.

Example · typescript
'use server'

import { after } from 'next/server'
import { cookies } from 'next/headers'
import { revalidateTag } from 'next/cache'

export async function publishPost(id: string) {
  await db.post.update({ where: { id }, data: { published: true } })
  revalidateTag('posts')

  after(async () => {
    const actor = (await cookies()).get('session-id')?.value ?? 'anonymous'
    await db.auditLog.create({ data: { action: 'publish', id, actor } })
    await fetch('https://hooks.example.com/published', {
      method: 'POST',
      body: JSON.stringify({ id }),
    })
  })
}

Discussion

  • Be the first to comment on this lesson.