Next.js 16 after(): фонові задачі після відповіді — повний посібник

Як використовувати after() у Next.js 16 для логування, аналітики та фонових задач після відповіді. Робочі приклади для Server Components, Route Handlers, Server Actions, proxy.ts, порівняння з waitUntil() і налаштування self-hosted розгортання.

Next.js 16 after() API: Background Tasks 2026

У кожному запиті до Next.js завжди ховається порція роботи, на яку користувачу взагалі немає сенсу чекати: запис у журнали, події в аналітику, розсилка листів, оновлення кешу, виклики вебхуків. У серверлес-середовищі є одна неприємна особливість — функція завершує роботу одразу після того, як відповідь відправлена. І все, що ви не встигли — просто гине разом із нею. Чесно кажучи, я колись витратив кілька годин, дебажучи саме це: лог-події «зникали» на проді, хоча локально все працювало. Саме цю проблему й розв'язує функція after(): вона дозволяє запланувати код, який виконається після того, як відповідь уже відправлено клієнту.

У Next.js 16 after() — це вже стабільний публічний API, який працює у Server Components, Route Handlers, Server Actions та (увага) proxy.ts. Цей посібник пояснює, як ним правильно користуватися, які пастки трапляються найчастіше, чим він відрізняється від waitUntil() та як налаштувати його для self-hosted розгортання.

Що таке after() і яку проблему вона розв'язує

Класичний обробник маршруту блокує відповідь, поки не закінчить усю роботу. Виглядає це приблизно так:

// Без after() — користувач чекає на все
export async function POST(request: Request) {
  const order = await createOrder();
  await sendConfirmationEmail(order);   // ~400 мс
  await pushToAnalytics(order);         // ~150 мс
  await writeAuditLog(order);           // ~80 мс
  return Response.json({ ok: true });   // лише тепер
}

Користувач отримує відповідь через ~700 мс, хоча сам факт створення замовлення зайняв якихось 50 мс. Емейли, аналітика, аудит — це сторонні ефекти, які жодним чином не впливають на UI. Чому ж тоді клієнт має на них чекати?

Перенесемо їх у фон через after():

import { after } from 'next/server';

export async function POST(request: Request) {
  const order = await createOrder();

  after(async () => {
    await sendConfirmationEmail(order);
    await pushToAnalytics(order);
    await writeAuditLog(order);
  });

  return Response.json({ ok: true }); // ~50 мс
}

Відповідь повертається миттєво, а фонові задачі продовжують жити своїм життям на сервері. Власне, для цього сценарію API і створено.

Чому не setTimeout чи fire-and-forget Promise?

На Vercel, AWS Lambda або Cloud Run серверлес-функція зупиняє виконання одразу після надсилання відповіді. Будь-який setTimeout чи незавершений Promise просто обривається — і ви ніколи про це не дізнаєтеся, поки не глянете на дашборд аналітики й не побачите дірку в даних. after() натомість повідомляє рантайм продовжити життєвий цикл функції до завершення коллбеку.

Де можна викликати after()

У Next.js 16 функція доступна в чотирьох місцях:

  • Server Components (включно з generateMetadata)
  • Route Handlers (app/api/.../route.ts)
  • Server Functions (Server Actions через 'use server')
  • proxy.ts (заміна middleware.ts у Next.js 16)

А от де не працює: у клієнтських компонентах, useEffect, getServerSideProps чи в старому pages/api.

Приклад 1: логування у Server Component

// app/dashboard/layout.tsx
import { after } from 'next/server';
import { logRender } from '@/lib/observability';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  after(() => {
    logRender({ route: '/dashboard', timestamp: Date.now() });
  });

  return <section className="dashboard">{children}</section>;
}

Важлива деталь: це не робить маршрут динамічним. Якщо /dashboard статичний, after() виконається на етапі білду або під час revalidate. Якщо динамічний — після надсилання HTML.

Важливе обмеження для Server Components

Усередині after() у Server Component не можна викликати cookies(), headers(), draftMode() чи searchParams. Причина суто технічна: Next.js повинен знати про доступ до request-даних ще до рендерингу, а after() виконується після нього. Тож якщо вам потрібен заголовок або кука — прочитайте їх перед викликом after() і передайте у замикання:

import { after } from 'next/server';
import { headers } from 'next/headers';

export default async function Page() {
  const userAgent = (await headers()).get('user-agent') ?? 'unknown';

  after(() => {
    // userAgent доступний через замикання
    logVisit({ userAgent });
  });

  return <h1>Привіт</h1>;
}

Приклад 2: аналітика у Route Handler

У Route Handlers та Server Actions обмеження м'якші: cookies() і headers() можна викликати безпосередньо у коллбеку after():

// app/api/checkout/route.ts
import { after } from 'next/server';
import { cookies, headers } from 'next/headers';
import { logUserAction } from '@/lib/audit';

export async function POST(request: Request) {
  const body = await request.json();
  const order = await createOrder(body);

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

    await logUserAction({
      type: 'checkout',
      orderId: order.id,
      userAgent,
      sessionId,
    });
  });

  return Response.json({ orderId: order.id });
}

Користувач отримує orderId миттєво, а аудит-лог пише сам собі вже після відповіді. Як на мене — ідеальний сценарій.

Приклад 3: Server Action із надсиланням листа

Найпоширеніший сценарій (і той, який, мабуть, має кожен проєкт) — після створення сутності надіслати привітальний лист. Без after() форма «зависає» на час доставки SMTP, і користувач починає клікати кнопку повторно.

// app/actions/signup.ts
'use server';

import { after } from 'next/server';
import { redirect } from 'next/navigation';
import { sendWelcomeEmail } from '@/lib/email';

export async function signupAction(formData: FormData) {
  const email = String(formData.get('email'));
  const user = await createUser({ email });

  after(async () => {
    await sendWelcomeEmail(user);
    await pushToCRM(user);
  });

  redirect('/welcome');
}

Зверніть увагу на цікаву деталь: after() виконується навіть якщо викликано redirect або notFound. І навіть якщо у головному обробнику стався виняток до повернення відповіді (за умови, що відповідь усе одно відправлена).

Приклад 4: after() у proxy.ts

У Next.js 16 middleware.ts перейменовано на proxy.ts (так, до цього треба звикнути). after() там теж працює — для лог-тегування та фонового моніторингу:

// proxy.ts
import { after, NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
  const start = performance.now();

  after(() => {
    const durationMs = performance.now() - start;
    metrics.histogram('proxy.duration_ms', durationMs, {
      path: request.nextUrl.pathname,
    });
  });

  return NextResponse.next();
}

Маленьке, але важливе застереження: усередині proxy.ts ви не можете змінити відповідь у коллбеку after() — вона вже надіслана. Тільки сторонні ефекти.

after() у статичних сторінках та PPR

Якщо after() викликається у статично згенерованій сторінці, коллбек запускається під час білду (або під час revalidate). У сценарії Partial Prerendering — після того, як динамічна частина у Suspense завершила рендер.

Невеликий приклад інтеграції з 'use cache':

// app/blog/[slug]/page.tsx
import { after } from 'next/server';
import { incrementViewCount } from '@/lib/stats';

async function getPost(slug: string) {
  'use cache';
  return db.posts.findFirst({ where: { slug } });
}

export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  after(() => incrementViewCount(slug));

  return <article><h1>{post.title}</h1></article>;
}

after() vs waitUntil(): у чому різниця

Це питання плутає більшість розробників — і не дивно. Якщо коротко: after() — це високорівнева абстракція Next.js поверх примітиву waitUntil().

Властивість after() waitUntil()
Походження Next.js (next/server) Платформа (Vercel, Cloudflare)
Рівень Високий, обгортка Низький примітив
Доступ до cookies/headers Так (у Route Handlers/Actions) Лише через стандартні API
Self-hosting Працює, якщо надати власну реалізацію waitUntil Лише на платформах, що його підтримують
Контекст застосування Server Components, Route Handlers, Actions, proxy.ts Будь-яка функція з доступом до контексту

Просте правило: якщо ви на Vercel або у Next.js — використовуйте after(). waitUntil() залишається корисним, коли ви пишете обробник за межами Next.js (наприклад, чистий Vercel Edge Function без Next.js).

Налаштування self-hosted розгортання

На Vercel after() працює без додаткових танців із бубном. Але якщо ви хостите Next.js самостійно (Docker, Cloud Run, Fly.io, VPS), то фреймворку потрібен спосіб дочекатися завершення фонових задач до зупинки процесу. Next.js шукає глобальний request-context із функцією waitUntil:

// instrumentation.ts (виконується один раз на старті)
import { AsyncLocalStorage } from 'node:async_hooks';

export async function register() {
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;

  const requestAsyncStorage = new AsyncLocalStorage();
  const inflight = new Set<Promise<unknown>>();

  globalThis[Symbol.for('@next/request-context')] = {
    get: () => ({
      waitUntil: (promise: Promise<unknown>) => {
        inflight.add(promise);
        promise.finally(() => inflight.delete(promise));
      },
    }),
  };

  // На SIGTERM — почекати на залишки фонових задач
  process.on('SIGTERM', async () => {
    await Promise.allSettled([...inflight]);
    process.exit(0);
  });
}

Якщо ви розгортаєте через output: 'standalone' у Docker, додайте reverse proxy (nginx) перед next start та переконайтеся, що orchestration (Kubernetes, Cloud Run) дає процесу достатньо часу на завершення (terminationGracePeriodSeconds ≥ 30). Інакше всі ваші фонові задачі будуть вбиті на півдорозі.

Обмеження, про які варто пам'ятати

  1. Тільки легкі задачі. after() не замінює черги повідомлень. Кодування відео, обробка великих файлів, повторні спроби з backoff — це робота для BullMQ, Inngest, Trigger.dev або SQS. after() добре підходить для одиничних HTTP-викликів і записів у БД до 1–2 секунд.
  2. Без гарантії доставки. Якщо коллбек кидає виняток або платформа примусово завершує процес, задача губиться. Не використовуйте after() для критичних операцій (списання коштів, фінансові транзакції) — це шлях до неприємних розмов із клієнтами.
  3. Жорсткий тайм-аут функції. На Vercel Hobby — до 10 секунд, на Pro — до 60 секунд (для Functions). Усе, що перевищує цей ліміт, буде обірване.
  4. Порядок не гарантований. Кілька викликів after() у одному запиті не обов'язково виконуються послідовно.
  5. Без модифікації відповіді. Усередині коллбеку відповідь уже надіслана — змінити статус, заголовки чи тіло вже неможливо.

Найкращі практики

  • Огортайте у try/catch. Помилка у after() не дійде до користувача, але має потрапити у систему моніторингу. Інакше це буде «тиха смерть», яку ви помітите тижнями пізніше.
  • Логуйте дублікати. При retry на рівні платформи коллбек може виконатися повторно — додайте ідемпотентні ключі.
  • Не забувайте про observability. Sentry, OpenTelemetry — підключайте їх у instrumentation.ts, щоб ловити помилки у фонових задачах.
  • Передавайте дані через замикання. Не намагайтеся читати request заново — він уже завершений.
  • Тестуйте на self-hosted збірці. Поведінка after() залежить від рантайму, а не від next dev.

Спільне використання з 'use cache'

У Next.js 16 директива 'use cache' кешує результати функцій. after() ідеально доповнює її для оновлення лічильників, не порушуючи кеш:

import { after } from 'next/server';
import { revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const post = await createPost(await request.json());

  after(async () => {
    revalidateTag(`post-${post.id}`);
    revalidateTag('posts-list');
    await pushToSearchIndex(post);
  });

  return Response.json({ id: post.id });
}

FAQ

Чи робить виклик after() маршрут динамічним?

Ні. after() не є request-time API. Якщо сторінка чи обробник статичні, вони такими й залишаться, а коллбек виконається під час білду чи revalidate.

Чи можна використовувати after() у клієнтських компонентах?

Ні. Функція доступна виключно на сервері: у Server Components, Route Handlers, Server Actions та proxy.ts. У 'use client' компонентах вона викине помилку.

Що станеться, якщо коллбек after() кине виняток?

Виняток не вплине на відповідь, яка вже відправлена. Однак сама задача обірветься — тож переконайтеся, що ви огорнули код у try/catch та надіслали помилку у Sentry або інший трекер.

Чи виконується after(), якщо викликано redirect() або notFound()?

Так. Документація Next.js підтверджує, що after() виконується навіть якщо відповідь завершилася через redirect, notFound або викид помилки до повернення основної відповіді.

after() чи окрема черга задач — що обрати?

Для легких задач до 1–2 секунд (логування, аналітика, надсилання одного листа) — after(). Для довготривалих, retry-чутливих або критичних задач (відеообробка, біллінг, масові імпорти) — спеціалізовані черги: Inngest, Trigger.dev, BullMQ, SQS.

Підсумок

after() — це інструмент, якого розробникам Next.js давно бракувало. Він закриває класичну проблему серверлес-середовищ: як виконати фонову роботу після відповіді, не змусивши користувача чекати. У Next.js 16 API нарешті стабільний, інтегрований з 'use cache', Partial Prerendering і новим proxy.ts.

Тож використовуйте after() для логування, аналітики, листів і оновлення кешу — і не намагайтеся замінити ним повноцінну чергу повідомлень. Перенесіть критичні операції у спеціалізовані інструменти, а after() залиште для того, для чого його створили: швидкої відповіді користувачу й чистого коду на сервері.

Про Автора Editorial Team

Our team of expert writers and editors.