Agent Skills: Sentry Rate Limits & Quota Optimization

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/sentry-rate-limits

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/sentry-pack/skills/sentry-rate-limits

Skill Files

Browse the full folder contents for sentry-rate-limits.

Download Skill

Loading file tree…

plugins/saas-packs/sentry-pack/skills/sentry-rate-limits/SKILL.md

Skill Metadata

Name
sentry-rate-limits
Description
|

Sentry Rate Limits & Quota Optimization

Overview

Manage Sentry rate limits, sampling strategies, and quota usage to control costs without losing visibility into critical errors. Covers client-side sampling, beforeSend filtering, server-side inbound filters, per-key rate limits, spike protection, and the usage stats API.

Prerequisites

  • Sentry account with a project DSN configured
  • SENTRY_AUTH_TOKEN with org:read and project:write scopes (Settings > Auth Tokens)
  • SENTRY_ORG and SENTRY_PROJECT slugs known
  • SDK installed: @sentry/node (npm) or sentry-sdk (pip)
  • Current event volume visible at sentry.io/stats/

Instructions

Step 1 — Understand Rate Limit Behavior

When your project exceeds its quota, Sentry returns 429 Too Many Requests with a Retry-After header. The SDK automatically stops sending events until the cooldown expires. Events generated during this window are permanently lost — there is no replay mechanism.

Rate limit tiers by plan:

| Plan | API Rate Limit | Notes | |------|---------------|-------| | Developer | 50 RPM | Shared quota, no reserved volume | | Team | 1,000 RPM | Per-organization, includes spike protection | | Business | 10,000 RPM | Per-organization, custom quotas available | | Enterprise | Custom | Negotiated per contract |

Quota categories (billed separately):

  • Errors — exceptions and log messages
  • Transactions — performance monitoring spans
  • Replays — session replay recordings
  • Attachments — file uploads (crash dumps, minidumps)
  • Profiles — continuous profiling data
  • Cron monitors — scheduled job check-ins

Rate limit headers returned on 429:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-Sentry-Rate-Limit-Limit: 50
X-Sentry-Rate-Limit-Remaining: 0
X-Sentry-Rate-Limit-Reset: 1711324800

Step 2 — Configure Client-Side Sampling

Sampling is the first line of defense. Set sampleRate for errors and tracesSampleRate for performance transactions.

TypeScript / Node.js:

import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,

  // Error sampling: 0.0 (drop all) to 1.0 (capture all)
  sampleRate: 0.25, // Capture 25% of errors

  // Transaction sampling: 0.0 to 1.0
  tracesSampleRate: 0.1, // Capture 10% of transactions

  // Dynamic transaction sampling — route-aware cost control
  tracesSampler: (samplingContext) => {
    const { name, parentSampled } = samplingContext;

    // Respect parent sampling decision in distributed traces
    if (parentSampled !== undefined) return parentSampled;

    // Drop health checks and readiness probes entirely
    if (name === 'GET /health' || name === 'GET /readiness') return 0;
    if (name?.includes('/health')) return 0;

    // High-value: payment and auth flows at 100%
    if (name?.includes('/api/payment') || name?.includes('/api/auth')) return 1.0;

    // Medium-value: API routes at 20%
    if (name?.startsWith('GET /api/') || name?.startsWith('POST /api/')) return 0.2;

    // Low-value: static assets — never trace
    if (name?.startsWith('GET /static/') || name?.startsWith('GET /assets/')) return 0;

    // Default fallback: 5%
    return 0.05;
  },
});

Python:

import sentry_sdk

def traces_sampler(sampling_context):
    tx_name = sampling_context.get("transaction_context", {}).get("name", "")

    # Drop health checks
    if "/health" in tx_name or "/readiness" in tx_name:
        return 0

    # High-value flows
    if "/api/payment" in tx_name or "/api/auth" in tx_name:
        return 1.0

    # API routes
    if tx_name.startswith(("GET /api/", "POST /api/")):
        return 0.2

    # Static assets
    if tx_name.startswith(("GET /static/", "GET /assets/")):
        return 0

    return 0.05

sentry_sdk.init(
    dsn=os.environ["SENTRY_DSN"],
    sample_rate=0.25,          # 25% of errors
    traces_sample_rate=0.1,    # 10% of transactions (fallback if no sampler)
    traces_sampler=traces_sampler,
)

Step 3 — Filter Noisy Errors with beforeSend

Use beforeSend to drop events before they count against your quota. This runs client-side, so filtered events never reach Sentry.

TypeScript / Node.js:

Sentry.init({
  dsn: process.env.SENTRY_DSN,

  beforeSend(event, hint) {
    const error = hint?.originalException as Error | undefined;

    // Drop browser extension errors (common in frontend SDKs)
    if (event.exception?.values?.some(e =>
      e.stacktrace?.frames?.some(f =>
        f.filename?.includes('extensions://') ||
        f.filename?.includes('moz-extension://') ||
        f.filename?.includes('chrome-extension://')
      )
    )) {
      return null; // Drop the event
    }

    // Drop known noisy browser errors
    if (error?.message?.match(/ResizeObserver loop/)) return null;
    if (error?.message?.match(/Non-Error promise rejection/)) return null;
    if (error?.name === 'AbortError') return null;
    if (error?.message?.match(/Load failed/)) return null;

    // CRITICAL: Always capture payment errors regardless of sampleRate
    if (error?.message?.includes('PaymentError') ||
        event.tags?.['transaction.type'] === 'payment') {
      return event; // Force capture
    }

    return event;
  },

  // Pattern-based error filtering (faster than beforeSend for known strings)
  ignoreErrors: [
    'ResizeObserver loop completed with undelivered notifications',
    'Non-Error promise rejection captured',
    /Loading chunk \d+ failed/,
    'Network request failed',
    'Failed to fetch',
    'AbortError',
    /^Script error\.?$/,
    'TypeError: cancelled',
    'TypeError: NetworkError when attempting to fetch resource',
  ],

  // Block errors originating from third-party scripts
  denyUrls: [
    /extensions\//i,
    /^chrome:\/\//i,
    /^chrome-extension:\/\//i,
    /^moz-extension:\/\//i,
    /hotjar\.com/,
    /google-analytics\.com/,
    /googletagmanager\.com/,
    /intercom\.io/,
  ],
});

Python:

def before_send(event, hint):
    if "exc_info" in hint:
        exc_type, exc_value, _ = hint["exc_info"]

        # Drop known noisy exceptions
        if exc_type.__name__ in ("ConnectionResetError", "BrokenPipeError"):
            return None

        # Drop health check 404s
        msg = str(exc_value)
        if "health" in msg.lower() and "404" in msg:
            return None

    # Always capture payment errors
    if event.get("tags", {}).get("transaction.type") == "payment":
        return event

    return event

sentry_sdk.init(
    dsn=os.environ["SENTRY_DSN"],
    before_send=before_send,
    ignore_errors=[
        "ConnectionResetError",
        "BrokenPipeError",
    ],
)

Step 4 — Enable Server-Side Inbound Data Filters

Inbound filters run on Sentry's servers before quota counting. Filtered events do not consume quota — this is free filtering.

Configure at Project Settings > Inbound Filters:

| Filter | What it blocks | Recommended | |--------|---------------|-------------| | Legacy browsers | IE 9/10, old Safari, old Android | Enable | | Browser extensions | Errors from browser extension code | Enable | | Localhost events | Events from localhost / 127.0.0.1 | Enable for production projects | | Web crawlers | Bot-generated errors (Googlebot, etc.) | Enable | | Filtered releases | Specific release versions | Use for deprecated releases | | Error message patterns | Custom regex patterns | Add known false-positive patterns |

Configure via API:

# Enable legacy browser filter
curl -X PUT \
  -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"active": true}' \
  "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/filters/legacy-browsers/"

# Enable browser extension filter
curl -X PUT \
  -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"active": true}' \
  "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/filters/browser-extensions/"

# Enable web crawler filter
curl -X PUT \
  -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"active": true}' \
  "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/filters/web-crawlers/"

Step 5 — Set Per-Key Rate Limits

Each DSN (Client Key) can have its own rate limit. This prevents a single project from exhausting the organization's entire quota.

Configure at Project Settings > Client Keys > Configure > Rate Limiting.

# Set rate limit to 1000 events per hour on a specific client key
# First, list client keys to find the key ID
curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/keys/" \
  | python3 -m json.tool

# Then set the rate limit
curl -X PUT \
  -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"rateLimit": {"window": 3600, "count": 1000}}' \
  "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/keys/$KEY_ID/"

Strategy for multi-environment setups:

  • Production DSN: 5,000 events/hour (critical errors matter)
  • Staging DSN: 500 events/hour (only need representative sample)
  • Development DSN: 100 events/hour (prevent local debugging floods)

Step 6 — Enable Spike Protection

Spike protection is auto-enabled on Team and Business plans. It detects sudden event volume increases and temporarily rate-limits the project to prevent quota exhaustion from error storms.

Configure at Organization Settings > Spike Protection.

When spike protection triggers:

  1. Sentry detects volume exceeding 10x normal baseline
  2. Events are temporarily dropped (429 returned to SDK)
  3. An email notification is sent to organization owners
  4. Protection auto-disables after the spike subsides

For programmatic spike alerts, set up a Sentry alert rule:

  • Condition: Number of events in project exceeds threshold
  • Action: Send notification to Slack/PagerDuty/email
  • Frequency: Alert once per hour

Step 7 — Monitor Quota Usage via API

# Organization-wide usage stats for the last 7 days, grouped by category
curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  "https://sentry.io/api/0/organizations/$SENTRY_ORG/stats_v2/?field=sum(quantity)&groupBy=category&interval=1d&statsPeriod=7d" \
  | python3 -m json.tool

# Per-project usage breakdown
curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  "https://sentry.io/api/0/organizations/$SENTRY_ORG/stats_v2/?field=sum(quantity)&groupBy=project&category=error&interval=1d&statsPeriod=7d" \
  | python3 -m json.tool

# Outcome-based stats (accepted, filtered, rate_limited, invalid)
curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  "https://sentry.io/api/0/organizations/$SENTRY_ORG/stats_v2/?field=sum(quantity)&groupBy=outcome&category=error&interval=1d&statsPeriod=24h" \
  | python3 -m json.tool

Step 8 — Reduce Payload Size and Deduplicate

Reduce event size and improve grouping to lower quota consumption:

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  maxBreadcrumbs: 30,   // Default: 100
  maxValueLength: 500,   // Default: 250

  beforeSend(event) {
    // Truncate large request bodies
    if (event.request?.data && typeof event.request.data === 'string') {
      event.request.data = event.request.data.substring(0, 1000);
    }
    // Remove cookies
    if (event.request?.cookies) delete event.request.cookies;

    // Custom fingerprinting: normalize dynamic values for better grouping
    if (event.exception?.values?.[0]) {
      const { type, value } = event.exception.values[0];
      const normalized = value
        ?.replace(/\b[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/gi, '<UUID>')
        ?.replace(/\b\d+\b/g, '<N>') || '';
      event.fingerprint = [type || 'unknown', normalized];
    }
    return event;
  },
});

Output

After completing these steps, you will have:

  • Sampling rates configured (sampleRate, tracesSampleRate, tracesSampler) to reduce event volume while preserving visibility into critical paths
  • beforeSend filtering dropping noisy browser errors (ResizeObserver, AbortError, extension errors) before they count against quota
  • ignoreErrors and denyUrls patterns blocking known false positives
  • Server-side inbound filters enabled for free pre-quota filtering
  • Per-key rate limits set to prevent single-project quota exhaustion
  • Spike protection enabled and alert rules configured
  • Quota monitoring via the /stats_v2/ API endpoint
  • Custom fingerprinting reducing event duplication

Error Handling

| Error | Cause | Solution | |-------|-------|----------| | 429 Too Many Requests | Quota exhausted for current billing period | Lower sampleRate and tracesSampleRate, add patterns to ignoreErrors, enable per-key rate limits | | Events silently dropped | Client SDK respecting Retry-After header | Reduce volume at source; SDK auto-recovers after cooldown expires | | Critical errors missed | sampleRate too low | Use beforeSend to force-return critical errors regardless of sampling; never filter payment/auth errors | | Quota exhausted early in period | No per-project rate limits | Set hourly rate limits per client key to spread quota evenly across the billing period | | Spike consuming entire quota | Spike protection not enabled or threshold too high | Enable spike protection in organization settings; set up volume alert rules | | 401 Unauthorized on stats API | Invalid or expired auth token | Regenerate token at Settings > Auth Tokens with org:read scope | | Inbound filter not reducing volume | Filter configured but wrong error type | Verify filter targets correct category; browser extension filter only works for JS SDK errors | | tracesSampler not called | tracesSampleRate takes precedence in some SDK versions | Remove tracesSampleRate when using tracesSampler — they conflict |

Examples

Example 1 — Quota Monitoring Bash Script

#!/usr/bin/env bash
# Monitor Sentry quota usage and alert if approaching limit
set -euo pipefail

ORG="${SENTRY_ORG:?Set SENTRY_ORG}"
TOKEN="${SENTRY_AUTH_TOKEN:?Set SENTRY_AUTH_TOKEN}"
THRESHOLD=${QUOTA_THRESHOLD:-80}  # Alert at 80% usage

accepted=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "https://sentry.io/api/0/organizations/$ORG/stats_v2/?field=sum(quantity)&groupBy=outcome&category=error&statsPeriod=1h&interval=1h" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for g in data.get('groups', []):
    if g['by'].get('outcome') == 'accepted':
        print(g['totals']['sum(quantity)']); break
else: print(0)")

echo "Accepted events (last hour): $accepted"
[ "$accepted" -gt "$THRESHOLD" ] && echo "WARNING: volume $accepted > threshold $THRESHOLD"

Example 2 — Enable All Inbound Filters via API

#!/usr/bin/env bash
set -euo pipefail
BASE="https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/filters"
AUTH="Authorization: Bearer $SENTRY_AUTH_TOKEN"

for filter in legacy-browsers browser-extensions web-crawlers; do
  curl -s -X PUT -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"active": true}' "$BASE/$filter/" && echo "Enabled: $filter"
done

Resources

Next Steps

  • Cost review: Query /stats_v2/ weekly to track spend trends by category
  • Alert rules: Create Sentry alerts for volume spikes per project
  • Replay sampling: Apply replaysSessionSampleRate and replaysOnErrorSampleRate for session replay cost control
  • Server-side sampling: Explore Sentry Dynamic Sampling (server-side) for organization-wide policies
  • Audit ignoreErrors: Review quarterly — patterns may suppress real bugs as code evolves