---
title: "Tracing"
description: "Monitor performance across all Next.js runtimes: client, server, and edge."
url: https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/
---

# Set Up Tracing for Next.js

Tracing captures the timing and flow of requests through your Next.js application. Learn more about [tracing](https://docs.sentry.io/concepts/key-terms/tracing.md) and how Sentry uses traces to track performance across services.

Sentry automatically instruments most operations, but Server Actions require manual setup.

## [Prerequisites](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#prerequisites)

Before enabling tracing, ensure you have:

* [Installed the Sentry SDK](https://docs.sentry.io/platforms/javascript/guides/nextjs.md) in your Next.js application
* Initialized Sentry in all three config files (`instrumentation-client.ts`, `sentry.server.config.ts`, `sentry.edge.config.ts`)

## [Enable Tracing](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#enable-tracing)

### [Configure Sample Rate](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#configure-sample-rate)

Add `tracesSampleRate` to your Sentry configuration in all three runtime files.

**Start with 100% in development, and 10-20% in production.** Adjust based on traffic volume and budget.

**\[Client] instrumentation-client.ts**

```typescript
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
});
```

**\[Server] sentry.server.config.ts**

```typescript
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
});
```

**\[Edge] sentry.edge.config.ts**

```typescript
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
});
```

## [What's Traced Automatically](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#whats-traced-automatically)

### [By Runtime](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#by-runtime)

The SDK instruments different operations depending on where your code runs:

| Runtime    | Auto-Instrumented                                   |
| ---------- | --------------------------------------------------- |
| **Client** | Page loads, navigations, fetch requests, Web Vitals |
| **Server** | API routes, Server Components, `getServerSideProps` |
| **Edge**   | Middleware, edge API routes                         |

```tsx
// All of these create spans automatically

// Client: navigation creates a span
<Link href="/dashboard">Dashboard</Link>;

// Server: API route creates a span
export async function GET() {
  return Response.json({ data });
}

// Server Component: creates a span
export default async function Page() {
  const data = await fetchData();
  return <div>{data}</div>;
}
```

For configuration options like timeouts, INP settings, and filtering spans, see [Automatic Instrumentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/instrumentation/automatic-instrumentation.md).

## [Server Actions](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#server-actions)

### [Why Manual Instrumentation?](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#why-manual-instrumentation)

Server Actions are **not** automatically instrumented. Without wrapping, they appear as anonymous server operations with no timing or context.

Use `withServerActionInstrumentation` to:

* Create named spans for each action
* Capture timing and errors
* Connect client and server traces via `headers`
* Attach form data to Sentry events via `formData`

```tsx
"use server";

import * as Sentry from "@sentry/nextjs";

export async function createOrder(formData: FormData) {
  return Sentry.withServerActionInstrumentation(
    "createOrder",
    async () => {
      // Your action logic
      const order = await db.orders.create({
        data: { items: formData.get("items") },
      });
      return { success: true, orderId: order.id };
    },
  );
}
```

### [With Headers and Form Data](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#with-headers-and-form-data)

Pass `headers` to connect client-side traces with server-side spans for full distributed tracing across the browser-to-server boundary. Pass `formData` to attach submitted form data to Sentry events for easier debugging.

```tsx
"use server";

import * as Sentry from "@sentry/nextjs";
import { headers } from "next/headers";

export async function submitForm(formData: FormData) {
  return Sentry.withServerActionInstrumentation(
    "submitForm",
    {
      headers: await headers(), // Connect client and server traces
      formData, // Attach form data to events
      recordResponse: true, // Include response data
    },
    async () => {
      // Action logic with full trace context
    },
  );
}
```

## [Web Vitals](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#web-vitals)

The SDK automatically captures [Web Vitals](https://docs.sentry.io/product/dashboards/sentry-dashboards/frontend/web-vitals.md) in the browser. These metrics measure real user experience:

| Metric   | What It Measures                               | Threshold (Good) |
| -------- | ---------------------------------------------- | ---------------- |
| **LCP**  | Largest Contentful Paint — loading performance | ≤ 2.5s           |
| **INP**  | Interaction to Next Paint — responsiveness     | ≤ 200ms          |
| **CLS**  | Cumulative Layout Shift — visual stability     | ≤ 0.1            |
| **FCP**  | First Contentful Paint — initial render        | ≤ 1s             |
| **TTFB** | Time to First Byte — server response           | ≤ 100ms          |

Web Vitals feed into your [Performance Score](https://docs.sentry.io/product/dashboards/sentry-dashboards/frontend/web-vitals.md#performance-score). In the default stream mode, LCP, CLS, and INP are sent as Web Vital spans.

To configure collection, see the [WebVitals integration](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/webvitals.md). For metric definitions and differences between SDK versions, see [Web Vitals Concepts](https://docs.sentry.io/product/dashboards/sentry-dashboards/frontend/web-vitals/web-vitals-concepts.md).

## [Custom Instrumentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#custom-instrumentation)

### [When to Use `startSpan`](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#when-to-use-startspan)

Add custom spans when you want to measure:

* Business operations (checkout flow, multi-step wizards)
* External API calls you want to track separately
* Database operations not auto-captured
* Expensive computations

For the full span API including `startSpanManual` and `startInactiveSpan`, see [Custom Instrumentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/instrumentation.md).

```tsx
import * as Sentry from "@sentry/nextjs";

export async function POST(request: Request) {
  return Sentry.startSpan(
    { name: "checkout.process", op: "checkout" },
    async () => {
      // Nested spans for sub-operations
      const cart = await Sentry.startSpan(
        { name: "checkout.validate_cart", op: "validation" },
        () => validateCart(request),
      );

      const payment = await Sentry.startSpan(
        { name: "checkout.process_payment", op: "payment" },
        () => processPayment(cart),
      );

      return Response.json({ orderId: payment.orderId });
    },
  );
}
```

### [Add Attributes](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#add-attributes)

Attach data to spans for filtering and debugging in Sentry.

```tsx
Sentry.startSpan(
  {
    name: "order.process",
    op: "order",
    attributes: {
      "order.id": orderId,
      "order.value": cart.total,
      "order.item_count": cart.items.length,
    },
  },
  async () => {
    // Operation logic
  },
);
```

## [Distributed Tracing](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#distributed-tracing)

### [Automatic Propagation](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#automatic-propagation)

The SDK automatically propagates trace context for:

* `fetch()` requests to your own domain
* Server Actions (when using `withServerActionInstrumentation` with headers)

For external APIs, configure `tracePropagationTargets`:

```typescript
Sentry.init({
  dsn: "https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
  tracesSampleRate: 0.1,

  // Propagate traces to these external services
  tracePropagationTargets: [
    "localhost",
    /^https:\/\/api\.yourcompany\.com/,
    /^https:\/\/payments\.stripe\.com/,
  ],
});
```

For CORS configuration and manual trace propagation (WebSockets, etc.), see [Distributed Tracing](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/distributed-tracing.md).

## [Agent Tracing](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#agent-tracing)

If you're building with agents or LLMs, you can extend your tracing setup to capture agent workflows, model calls, tool executions, and token usage. With tracing already enabled, the SDK can automatically instrument supported AI libraries on the server and connect agent activity to the rest of your application traces.

[Set up Agent Tracing](https://docs.sentry.io/platforms/javascript/guides/nextjs/agent-tracing.md) to get started.

## [Quick Reference](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#quick-reference)

| Scenario                   | Auto-Instrumented? | Action                                |
| -------------------------- | ------------------ | ------------------------------------- |
| Page loads and navigations | Yes                | None                                  |
| API routes (`route.ts`)    | Yes                | None                                  |
| Server Components          | Yes                | None                                  |
| `getServerSideProps`       | Yes                | None                                  |
| **Server Actions**         | **No**             | Use `withServerActionInstrumentation` |
| External API calls         | Partial            | Configure `tracePropagationTargets`   |
| Custom business logic      | No                 | Use `startSpan`                       |

## [Troubleshooting](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#troubleshooting)

### [High trace volume](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#high-trace-volume)

If you're sending too many traces, lower your sample rate or use `tracesSampler` for dynamic sampling based on route.

See [Configure Sampling](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/configure-sampling.md) for more options.

```typescript
Sentry.init({
  tracesSampler: ({ name }) => {
    // Sample 5% of health checks
    if (name.includes("health")) return 0.05;
    // Sample 50% of API routes
    if (name.includes("/api/")) return 0.5;
    // Default 10%
    return 0.1;
  },
});
```

### [Detached spans with `after()` or `waitUntil()`](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing.md#detached-spans-with-after-or-waituntil)

If you're using Next.js's [`after()`](https://nextjs.org/docs/app/api-reference/functions/after) or [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) and you see multiple root spans (like individual `db` spans) instead of one cohesive trace, your spans are becoming detached.

This happens when `after()` or `waitUntil()` runs after the response is sent. By then, the request's root span has already ended.

Wrap your deferred work with `startSpan` inside the `after()` or `waitUntil()` callback to group all background spans into a single transaction:

```tsx
import { after } from "next/server";
import { waitUntil } from "@vercel/functions";
import * as Sentry from "@sentry/nextjs";

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

  after(
    Sentry.startSpan({ name: "webhook.process", op: "task" }, () =>
      processWebhook(data),
    ),
  );

  waitUntil(
    Sentry.startSpan({ name: "webhook.process", op: "task" }, () =>
      processWebhook(data),
    ),
  );

  return Response.json({ received: true });
}
```

## Pages in this section

- [Set Up Distributed Tracing](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/distributed-tracing.md)
- [Configure Sampling](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/configure-sampling.md)
- [Streamed Spans](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/streamed-spans.md)
- [Streamed Spans](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/streamed-spans__v10.x.md)
- [Instrumentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/instrumentation.md)
- [Troubleshooting](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/troubleshooting.md)
