Type something to search...
Nextjs after function

Nextjs after function

Some work genuinely shouldn't block a response — logging an analytics event, firing a webhook notification, writing an audit log entry. The user doesn't need to wait for any of that to complete before seeing their page or getting their API response. after() is Next.js's purpose-built answer: schedule a callback to run once the response (or prerender) has already finished, so it executes without adding a single millisecond to what the user actually experiences.

Basic Usage

import { after } from "next/server";
import { log } from "@/app/utils";

export default function Layout({ children }: { children: React.ReactNode }) {
  after(() => {
    // Executes after the layout is rendered and sent to the user
    log();
  });
  return <>{children}</>;
}

after works in Server Components (including generateMetadata), Server Functions, Route Handlers, and Proxy — essentially everywhere on the server side that has a response to defer work past.

The Detail That Matters Most: after Doesn't Make a Route Dynamic

This is worth stating plainly because it's counterintuitive at first: after is not a Request-time API, and calling it does not opt a route into dynamic rendering. If you use after inside an otherwise fully static page, the callback still executes — just at build time, or whenever that page is next revalidated, rather than per individual request. This distinction matters enormously for anyone trying to reason about whether adding after() somewhere will silently change a page's caching/rendering strategy — it won't, by itself.

Duration and Timeout Behavior

after runs for whatever duration your platform's default (or explicitly configured) route timeout allows — it doesn't get its own separate, unlimited execution budget. If your platform supports it, the maxDuration route segment config is the lever for adjusting that ceiling.

Behavior Worth Knowing Before You Rely on It

after executes even when the response itself didn't complete successfully — including cases where an error was thrown, or where notFound() or redirect() was called partway through rendering. This is a deliberate design choice: logging and analytics work scheduled via after shouldn't silently vanish just because the request it was attached to ended in an error state or a redirect rather than a normal success response.

React's cache function deduplicates work called inside after, the same as it does elsewhere — useful if multiple deferred callbacks might otherwise redundantly re-fetch the same underlying data.

after calls can nest inside other after calls. This opens up a genuinely useful pattern: build small utility functions that wrap after internally to add cross-cutting behavior (timing, error swallowing, retry logic) around whatever callback you actually pass in, without every call site needing to reimplement that wrapping logic itself.

Request APIs Inside after: It Depends Entirely on Context

This is the single most consequential detail in this whole reference, and it differs based on where after is called from.

In Route Handlers and Server Functions: Direct Access Works

import { after } from "next/server";
import { cookies, headers } from "next/headers";
import { logUserAction } from "@/app/utils";

export async function POST(request: Request) {
  // Perform mutation
  // ...

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

  return new Response(JSON.stringify({ status: "success" }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
}

Calling cookies() and headers() directly inside the after callback works fine here — genuinely useful for logging exactly what request triggered a given mutation, without needing to capture that data beforehand.

In Server Components: Direct Access Throws

Server Components — pages, layouts, and generateMetadatacannot call cookies, headers, or other Request-time APIs from inside an after callback. The reason is structural, not arbitrary: Next.js needs to know, during React's own rendering lifecycle, exactly which part of the component tree accesses request data, in order to support Partial Prerendering and Cache Components correctly. But after runs after that rendering lifecycle has already finished — by the time your callback executes, the window during which Next.js was tracking request-data access has already closed. Calling cookies() or headers() inside an after callback in a Server Component throws a runtime error, not a silent no-op.

The fix is straightforward once you know the rule: read the request data before calling after, during the component's normal render, and pass the already-resolved values into the callback via closure:

import { after } from "next/server";
import { cookies, headers } from "next/headers";
import { logUserAction } from "@/app/utils";

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

  after(() => {
    // Use the already-resolved values — no request API calls in here
    logUserAction({ sessionCookie, userAgent });
  });

  return <h1>My Page</h1>;
}

With Cache Components: Combine after With a Suspense Boundary

Under Cache Components, any component reading request data must itself be wrapped in <Suspense> so the rest of the page can still be prerendered into a static shell. The pattern that reconciles this with after is reading the request data inside that specific dynamic (Suspense-wrapped) component, and scheduling the after call from there:

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>;
}

Here, <h1> and the Suspense fallback are both part of the static shell; DynamicContent reads the cookie during its own render (outside the after callback, following the same rule as above) and passes the resolved value in via closure. Because cookies() is called during rendering, not inside after itself, this pattern is fully valid even under the stricter Cache Components model.

Platform Support

Deployment OptionSupported
Node.js serverYes
Docker containerYes
Static exportNo
AdaptersPlatform-specific

A static export produces fixed HTML with no server process left running afterward to execute a deferred callback against — so after has nothing to attach to in that deployment mode.

Making after Work on Serverless Platforms

This is genuinely advanced material, relevant mainly if you're building a custom adapter or self-hosting on infrastructure Next.js doesn't already support out of the box. Serverless functions typically terminate the instant a response is sent — there's no lingering process for a deferred callback to execute inside, unless the platform provides a mechanism to explicitly extend the invocation's lifetime. Vercel (and Next.js's own built-in support) uses a primitive called waitUntil(promise) for exactly this: it keeps a serverless invocation alive until every promise passed to it has settled.

If you're implementing platform support yourself, Next.js expects to find this capability via a specific global:

const RequestContext = globalThis[Symbol.for("@next/request-context")];
const contextValue = RequestContext?.get();
const waitUntil = contextValue?.waitUntil;

Which means globalThis[Symbol.for('@next/request-context')] needs to expose an object matching this shape:

type NextRequestContext = {
  get(): NextRequestContextValue | undefined;
};

type NextRequestContextValue = {
  waitUntil?: (promise: Promise<any>) => void;
};

A minimal implementation using Node's AsyncLocalStorage to thread a per-request waitUntil through:

import { AsyncLocalStorage } from "node:async_hooks";

const RequestContextStorage = new AsyncLocalStorage<NextRequestContextValue>();

const RequestContext: NextRequestContext = {
  get() {
    return RequestContextStorage.getStore();
  },
};
globalThis[Symbol.for("@next/request-context")] = RequestContext;

const handler = (req, res) => {
  const contextValue = { waitUntil: YOUR_WAITUNTIL };
  return RequestContextStorage.run(contextValue, () => nextJsHandler(req, res));
};

This is squarely infrastructure-team territory rather than everyday application code — most projects deploying to a platform Next.js already supports (Vercel, or any adapter implementing this contract) never need to touch this directly.

Version History

VersionChanges
v15.1.0after became stable
v15.0.0-rcunstable_after introduced

Key Takeaways

BehaviorDetail
Doesn't affect rendering strategyNot a Request-time API — doesn't force a static page dynamic
Runs even on failureExecutes after errors, notFound(), and redirect() alike
TimeoutBound by the platform's route timeout / maxDuration
Route Handlers / Server FunctionsCan call cookies()/headers() directly inside the callback
Server ComponentsMust read request data before calling after, then pass it in via closure — direct access inside the callback throws
Cache ComponentsCombine with a <Suspense>-wrapped dynamic component, reading request data there
Static exportNot supported — no lingering process to run deferred work

after solves a genuinely common problem — deferred, non-blocking side effects — with a clean API, but the Server-Component request-data restriction is the one rule worth internalizing before you reach for it: read what you need during render, pass it in, and let the callback stay a pure consumer of already-resolved values.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper