Skip to content

Trace sample rates that do not bankrupt you

Errors are cheap and spans are not. How to pick tracesSampleRate, why the edge runtime needs a lower one, and how to keep the traces you actually need while dropping 95% of the rest.

Sentry4 min readships at docs/solutions/sentry/trace-sample-rates-that-do-not-bankrupt-you.md

Tags: sentry · tracing · performance · cost · sampling

You follow the setup guide, it says tracesSampleRate: 1.0, you ship. Traffic grows. A month later the bill has a line item for spans that dwarfs the errors, and nobody has opened the Performance tab since the first week.

The mistake is treating one number as a setting rather than a budget. Errors and traces are priced differently and used differently, and the sample rate is where that difference gets decided.

The unit economics

Errors are rare by definition. A healthy app produces a handful per thousand requests, and each one is individually valuable: you want all of them.

Transactions and spans are produced by every request, whether anything interesting happened or not. Each page load is a transaction; each fetch, database query and component render inside it is a span. One request can easily be twenty spans.

So at tracesSampleRate: 1.0, an app doing a million requests a month produces a million transactions and perhaps twenty million spans. That is the number that shows up on the invoice, and 99.9% of it describes requests that were completely fine.

The wrong way

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
});

Copied from a quickstart, shipped to production, never revisited. Two problems beyond cost: at full sampling the SDK does measurably more work per request, and the Performance tab becomes an undifferentiated wall that nobody reads.

The right way: a small constant, raised deliberately

// sentry.server.config.ts
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1,

10% in production is a good starting point for a normal web app. It is enough to see p95 latency and to spot a slow route, and it costs a tenth of the naive setting.

Then adjust with two facts:

  • Traffic volume. Under ~10k requests/day, 10-20% is fine. Over ~1M/day, you want 1% or less; you have plenty of samples either way.
  • What you actually look at. If nobody has opened Performance in a month, go to 0.01 or turn tracing off. You can raise it in an afternoon when you need it.

The edge runtime needs its own, lower number

// sentry.edge.config.ts
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.02 : 1,

Edge code (proxy, middleware, edge routes) runs on far more requests than your page views. A matcher that is slightly too broad puts it in front of every static asset, prefetch and health check. Sampling edge transactions at the same rate as page transactions can easily produce ten times the volume for a tenth of the insight.

Sample by route, not uniformly

The real win is tracesSampler, which decides per transaction. Now you can keep 100% of the things you care about and almost none of the rest:

Sentry.init({
  tracesSampler: (samplingContext) => {
    const name = samplingContext.name;

    // Never trace health checks or the Sentry tunnel itself.
    if (name.includes("/api/health") || name.includes("/monitoring")) return 0;

    // Checkout is where latency costs money. Keep all of it.
    if (name.includes("/checkout") || name.includes("/api/webhooks")) return 1;

    // Inherit the parent's decision on distributed traces, so a sampled
    // request is not half-recorded.
    if (typeof samplingContext.parentSampleRate === "number") {
      return samplingContext.parentSampleRate;
    }

    return 0.05;
  },
});

Three rules of thumb encoded there:

  1. Drop what is never interesting. Health checks, uptime pollers, the tunnel route, static asset requests. These are pure volume.
  2. Keep everything on the paths that matter. Checkout, signup, payment webhooks: the handful of routes where a latency regression has a cost you can name.
  3. Respect the parent decision. A partially sampled distributed trace is worse than no trace: the waterfall has holes and you draw wrong conclusions.

Errors are never sampled by this

A frequent misunderstanding: tracesSampleRate: 0.1 does not mean you lose 90% of your errors. It only affects performance transactions. Error events have their own control, sampleRate, which should stay at 1.0: errors are the cheap, high-value data.

If your error volume is genuinely too high, the fix is filtering the noise (ignoreErrors, denyUrls, better fingerprints), not sampling away real bugs at random.

Session Replay is the expensive one

Replay is priced separately and is heavier than everything else: it records the DOM, uploads it, and adds substantially to the client bundle. This battery ships with both replay rates at 0 deliberately.

If you enable it:

replaysSessionSampleRate: 0,     // do not record ordinary sessions
replaysOnErrorSampleRate: 0.1,   // record a fraction of sessions that errored

Error-triggered replay is the only variant that pays for itself: you get the recording for the sessions you actually want to watch. And configure masking before you enable anything: replay records what users type.

Monitoring the budget

  • Set a spend cap in Sentry's billing settings, and per-category limits if your plan supports them. Without one, a retry storm can burn a month's quota overnight.
  • Check the Stats page monthly: accepted vs dropped events per category. A spike in transactions is usually a new route with a chatty client, not real growth.
  • When you hit a limit, Sentry drops events, and it does not preferentially keep the errors. A transaction flood can cost you the error you needed.

The starting point to copy

For a normal app, before you have data:

SettingServerEdgeClient
sampleRate (errors)1.01.01.0
tracesSampleRate0.10.020.1
replaysSessionSampleRatenonenone0
replaysOnErrorSampleRatenonenone0

Then, one month in: look at what you have opened. Raise the rate on the two routes you investigated, drop it everywhere else, and delete the tracing you never read.