Trimming Sentry event volume with sampleRate and beforeSend
Sentry's sampleRate and beforeSend hook control what leaves your app, but they do different jobs. And the backend can still drop events. Here's how to configure and verify both.
17 Jul 2025, 11:38 UTC

The problem: a useful dashboard that costs too much
A service handling a few hundred requests per second can produce thousands of Sentry error events per hour from a single noisy code path — a retry loop, a flaky upstream, or a validation branch that logs at error level. The dashboard becomes hard to read, and the project quota drains before the month ends. The useful takeaway: sampleRate and beforeSend are two different controls, and treating them as one knob is the usual reason teams either lose visibility or keep paying for noise.
Two knobs with different jobs
sampleRate is a number between 0 and 1 set at SDK initialization. It applies to error events: with sampleRate: 0.2, roughly one in five eligible events is sent, chosen at random on the client. It is a blunt volume control — it does not care which error it drops.
beforeSend is a callback that receives each event object just before it would be transmitted. Returning the event sends it; returning null discards it. Because you see the whole event, you can filter on event.level, exception type, tags, or request URL, and you can also add tags or rewrite fields. This is the place for rules like "drop info-level events" or "drop errors from the health-check route".
Two related settings are easy to confuse with these. tracesSampleRate controls performance transactions, not errors, and beforeSendTransaction is the matching hook for transactions (JavaScript SDK v7 and later). If your bill is driven by transactions, tuning sampleRate will not help.
A worked example
Run this in your application's Sentry initialization module — the single file that calls Sentry.init(). It needs no special permissions; it is ordinary application code. Replace the DSN and adjust the rates for your traffic.
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
sampleRate: 0.25, // keep roughly 1 in 4 error events
tracesSampleRate: 0.05, // transactions are usually far more numerous
beforeSend(event) {
// Discard low-severity noise before it counts against quota.
if (event.level === "info" || event.level === "debug") {
return null;
}
// Drop errors from a known-noisy health check path.
const url = event.request && event.request.url;
if (url && url.includes("/healthz")) {
return null;
}
// Enrich what survives.
event.tags = { ...event.tags, release_channel: process.env.CHANNEL };
return event;
},
});
Keep the callback synchronous and cheap. In most SDKs it runs on the event-sending path, so an await or a network call inside it can delay or drop delivery. If you need asynchronous enrichment, compute it earlier and attach the result as a tag or context.
Ordering is also worth knowing: both the sampling decision and beforeSend happen client-side, but the exact order between them is an SDK implementation detail. Do not write a beforeSend that assumes sampling has already run.
Verifying what actually leaves the app
Configuration that looks right is not evidence. A practical check:
- Capture a known event from the same process, for example
Sentry.captureMessage("sampling-check"), and log the value it returns. Recent JavaScript SDKs return an event ID string; if yours does not, log a timestamp instead. - Search the Sentry UI for that event ID, or for the message text, within the right project and time window.
- If it is absent, it was sampled out, filtered by
beforeSend, or dropped before transmission. Repeat a few times withsampleRate: 1to separate "filtered" from "sampled". - Compare the project's quota and usage graphs over a full day against the rate you configured. A large gap usually points to server-side rate limiting rather than a broken SDK.
Newer SDKs also emit client reports — a summary of how many events were dropped client-side and for what reason. Where available, that is a faster signal than counting events by hand.
Trade-offs and limits
- Sampling is random, so rare bugs can be missed entirely at low rates. Filtering with
beforeSendis deterministic and is usually the better first move for noise you can name. - Sentry's backend applies its own rate limits when a project nears its quota and can discard events the SDK allowed. Your dashboard will then show fewer issues than your configuration predicts.
- Inconsistent initialization — a second
Sentry.init()in a worker, or a lazily imported module with different options — produces uneven sampling and misleading coverage numbers. Keep initialization in one place. - Every filter you add is a blind spot. If you need an audit trail, log discarded events to your own logging stack instead of relying on Sentry.
What to do next
Start with beforeSend filters for noise you can name, then set sampleRate to whatever the remaining volume allows. Leave tracesSampleRate low by default. After a day, compare the quota graph with your configured rates and adjust once; chasing daily fluctuations usually makes coverage worse, not better.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.