Manage scale Sentry for high-traffic applications.
Use when optimizing for high event volumes,
managing costs at scale, or tuning for performance.
Trigger with phrases like "sentry high traffic", "scale sentry",
"sentry high volume", "sentry millions events".
Configure Sentry for applications processing 1M+ requests/day without sacrificing error visibility, burning through quota, or adding measurable SDK overhead. Covers adaptive sampling, connection pooling, multi-region tagging, quota management, SDK benchmarking, batch submission, load testing, and self-hosted deployment considerations.
Prerequisites
Application handling sustained high traffic (>10K requests/min or >1M events/day)
Sentry organization with quota and billing access (Settings > Subscription)
@sentry/node v8+ installed (npm ls @sentry/node)
Performance baseline established (p50/p95/p99 latency without Sentry)
Event volume estimates calculated per category (errors, transactions, replays, attachments)
Instructions
Step 1 — Implement Adaptive Sampling
Static tracesSampleRate wastes quota at scale because it treats a health check the same as a checkout. Replace it with a traffic-aware tracesSampler that adjusts rates based on endpoint criticality and current load.
Traffic-aware tracesSampler:
import * as Sentry from '@sentry/node';
// Track request volume per endpoint for adaptive rate adjustment
const endpointVolume = new Map<string, { count: number; resetAt: number }>();
const WINDOW_MS = 60_000;
function getAdaptiveRate(name: string, baseRate: number): number {
const now = Date.now();
let entry = endpointVolume.get(name);
if (!entry || now > entry.resetAt) {
entry = { count: 0, resetAt: now + WINDOW_MS };
endpointVolume.set(name, entry);
}
entry.count++;
// Scale down sampling as volume increases within window
// 0-100 req/min: full base rate
// 100-1000: halve it
// 1000+: quarter it
if (entry.count > 1000) return baseRate * 0.25;
if (entry.count > 100) return baseRate * 0.5;
return baseRate;
}
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampler: (samplingContext) => {
const { name, parentSampled } = samplingContext;
// Always respect parent decision for distributed tracing consistency
if (parentSampled !== undefined) return parentSampled ? 1.0 : 0;
// Tier 0: Never sample — high-frequency, zero diagnostic value
if (name?.match(/\/(health|ready|alive|ping|metrics|favicon)/)) return 0;
if (name?.match(/\.(css|js|png|jpg|svg|woff2?|ico)$/)) return 0;
// Tier 1: Always sample — business-critical, low volume
if (name?.includes('/payment') || name?.includes('/checkout')) return 1.0;
if (name?.includes('/auth/login')) return getAdaptiveRate('auth', 0.5);
// Tier 2: Moderate sampling — API mutations (higher signal)
if (name?.startsWith('POST /api/')) return getAdaptiveRate(name, 0.05);
if (name?.startsWith('PUT /api/')) return getAdaptiveRate(name, 0.05);
if (name?.startsWith('DELETE /api/')) return getAdaptiveRate(name, 0.05);
// Tier 3: Light sampling — API reads
if (name?.startsWith('GET /api/')) return getAdaptiveRate(name, 0.02);
// Tier 4: Background jobs — sample sparingly
if (name?.startsWith('job:') || name?.startsWith('queue:')) {
return getAdaptiveRate(name, 0.01);
}
// Tier 5: Everything else — minimal baseline
return getAdaptiveRate(name || 'default', 0.005);
},
});
At high throughput, every byte and every millisecond of SDK processing matters. This configuration reduces memory footprint, payload size, and CPU time.
Lean SDK initialization:
import * as Sentry from '@sentry/node';
import os from 'node:os';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'production',
release: `${process.env.SERVICE_NAME}@${process.env.VERSION || 'unknown'}`,
// --- Memory reduction ---
maxBreadcrumbs: 15, // Down from 100 default; saves ~85KB/scope
maxValueLength: 200, // Truncate long string values
// --- Disable high-overhead integrations ---
integrations: (defaults) => defaults.filter(i =>
!['Console', 'ContextLines'].includes(i.name)
),
// --- No profiling at high scale (use dedicated APM if needed) ---
profilesSampleRate: 0,
// --- Transport tuning for high-throughput ---
transportOptions: {
bufferSize: 100, // Default 64; absorbs traffic spikes
},
// --- Context size limiter ---
beforeSend(event) {
// Truncate oversized contexts to prevent payload bloat
if (event.contexts) {
for (const [key, ctx] of Object.entries(event.contexts)) {
const str = JSON.stringify(ctx);
if (str.length > 2000) {
event.contexts[key] = { _truncated: true, originalSize: str.length };
}
}
}
// Strip headers that add bulk without diagnostic value
if (event.request?.headers) {
const keep = ['content-type', 'accept', 'user-agent', 'x-request-id'];
event.request.headers = Object.fromEntries(
Object.entries(event.request.headers)
.filter(([k]) => keep.includes(k.toLowerCase()))
);
}
return event;
},
// --- Multi-region tags for infrastructure visibility ---
serverName: process.env.HOSTNAME || process.env.POD_NAME || os.hostname(),
initialScope: {
tags: {
region: process.env.AWS_REGION || process.env.GCP_REGION || 'unknown',
cluster: process.env.K8S_CLUSTER || 'default',
pod: process.env.POD_NAME || 'unknown',
service: process.env.SERVICE_NAME || 'unknown',
},
},
});
Graceful shutdown ensuring event delivery:
import * as Sentry from '@sentry/node';
async function shutdown(signal: string) {
console.log(`${signal} received — flushing Sentry events`);
// Stop accepting new requests
server.close();
// Flush all pending events (2s timeout prevents hanging deploys)
const flushed = await Sentry.close(2000);
if (!flushed) {
console.warn('Sentry flush timed out — some events may be lost');
}
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Step 3 — Manage Quotas, Test Under Load, and Plan for Scale
Quota management and reserved volume pricing:
Application: 10M requests/day, 0.1% error rate, @sentry/node v8
Error events (with adaptive beforeSend):
Raw errors: 10M x 0.001 = 10,000/day
After dedup: ~1,000/day (90% reduction) = 30K/month
Transaction events (with tiered tracesSampler):
Health/static: 0% of 4M = 0
Payment (T1): 100% of 5K = 5,000/day
POST API (T2): 5% of 500K = 25,000/day
GET API (T3): 2% of 5M = 100,000/day
Other (T5): 0.5% of 500K = 2,500/day
Total: ~132K/day = 4M/month
Sentry Business plan ($26/mo base):
Errors: 30K included in base plan
Transactions: 100K included, overage 3.9M x $0.000025 = ~$97/mo
Estimated total: ~$123/month for 10M requests/day
Reserved volume (if predictable traffic):
5M txns/mo reserved = $80/mo (vs $97 on-demand)
Saves ~$17/mo, locks in price for 12 months
→ Total: ~$106/month
SDK overhead benchmarks:
// Measure SDK initialization cost
const initStart = performance.now();
Sentry.init({ /* ... */ });
const initMs = performance.now() - initStart;
console.log(`Sentry.init: ${initMs.toFixed(1)}ms`);
// Expected: 5-15ms (Node.js), acceptable <50ms
// Measure per-request overhead with Sentry vs without
import { performance, PerformanceObserver } from 'node:perf_hooks';
async function benchmarkOverhead(iterations: number = 1000) {
// Baseline: request without Sentry instrumentation
const baseStart = performance.now();
for (let i = 0; i < iterations; i++) {
await handleRequest({ path: '/api/test', method: 'GET' });
}
const baseMs = (performance.now() - baseStart) / iterations;
// Instrumented: request with Sentry span
const sentryStart = performance.now();
for (let i = 0; i < iterations; i++) {
await Sentry.startSpan(
{ name: 'GET /api/test', op: 'http.server' },
() => handleRequest({ path: '/api/test', method: 'GET' })
);
}
const sentryMs = (performance.now() - sentryStart) / iterations;
console.log(`Baseline: ${baseMs.toFixed(3)}ms/req`);
console.log(`With Sentry: ${sentryMs.toFixed(3)}ms/req`);
console.log(`Overhead: ${(sentryMs - baseMs).toFixed(3)}ms (${(((sentryMs - baseMs) / baseMs) * 100).toFixed(1)}%)`);
// Healthy: <0.5ms overhead per request, <2% CPU impact
}
Kafka: KAFKA_NUM_PARTITIONS: 32 (match to consumer count)
Snuba: 4+ consumer replicas for Clickhouse ingestion parallelism
Clickhouse: 16G+ RAM, dedicated SSD volumes
Self-hosted vs SaaS break-even:
SaaS at 100M events/month: ~$2,500/mo (Business plan + overage)
Self-hosted (3x r6g.2xlarge): ~$1,200/mo infra + $800/mo ops (0.25 FTE)
Break-even: ~50M events/month
→ Use SaaS up to 50M events; evaluate self-hosted above that
Output
Adaptive sampling reducing duplicate error volume by 90%+ while preserving first-occurrence fidelity
Traffic-aware tracesSampler with 5 tiers adjusting dynamically based on endpoint volume
SDK memory and CPU footprint minimized (15 breadcrumbs, truncated contexts, filtered headers)
Connection pooling via persistent HTTPS agent for efficient event submission
Multi-region infrastructure tags for filtering by region/cluster/pod in Sentry dashboard
Cost model with reserved volume pricing showing $106/month for 10M requests/day
k6 load test script validating Sentry overhead stays under 5ms at p95
Batch job processing pattern with scope isolation and periodic flush
Self-hosted vs SaaS break-even analysis for enterprise decision-making
Error Handling
Error
Cause
Solution
Events silently dropped
SDK buffer full during traffic spike
Increase transportOptions.bufferSize to 200+, verify network to Sentry ingest
429 rate limit from Sentry
Quota exhausted or spike protection triggered
Enable spike protection in Settings > Subscription, reduce sample rates
Memory growing linearly over time
Breadcrumb or scope accumulation
Reduce maxBreadcrumbs, verify withScope is used (not configureScope)