getsentry / getsentry/sentry-mcp

Investigate rate limiting on MCP API calls after authentication

Open
#844 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
853
Forks
144
Avg merge
19h 25m
Merged PRs (30d)
32

Description

## Summary

Customers using Cursor Automations with the Sentry MCP server are hitting rate limit errors shortly after authenticating. Reports include: all API calls being rate-limited within a few minutes of auth, and automation runs being blocked with `"The automation was rate-limited due to too many concurrent runs."`

## Symptoms

- Rate limit errors triggered within minutes of initial authentication
- Affects all API calls (not isolated to a specific endpoint)
- Observed in concurrent/parallel automation run scenarios (e.g. Cursor Automations)
- Error: `"The automation was rate-limited due to too many concurrent runs. Retry after a short delay or reduce the number of parallel automation runs."`

## How Rate Limiting Is Implemented

Rate limiting runs in the Cloudflare Worker (`packages/mcp-cloudflare`) via Cloudflare's native `RateLimit` binding. There are two independent layers:

### Layer 1: IP-based (pre-auth)
- Applied to all `/mcp` and `/oauth` routes **before** OAuth processing
- Binding: `MCP_IP_RATE_LIMITER` (fallback: `MCP_RATE_LIMITER`)
- Key: `mcp:ip:[0:16]`
- Threshold: **300 requests / 60 seconds** per IP
- Source: `src/server/index.ts`

### Layer 2: Per-user (post-auth)
- Applied after OAuth token validation, inside the MCP handler
- Binding: `MCP_USER_RATE_LIMITER` (fallback: `MCP_RATE_LIMITER`)
- Key: `mcp:user:[0:16]`
- User ID = **Sentry user ID** (from OAuth token, `payload.user.id`) — shared across all clients/tokens for the same Sentry account
- Threshold: **60 requests / 60 seconds** per user
- Source: `src/server/lib/mcp-handler.ts`

### Key implementation details
- Rate limit keys use the first 16 hex chars of SHA-256 of the identifier (privacy-preserving, but with a ~1/10^19 collision risk — negligible)
- **Both bindings ARE defined in `wrangler.jsonc`** for prod and canary — they are wired. However the namespace IDs in the file (`1001`–`1004`, `2001`–`2004`) are local dev mock IDs. Production bindings must be configured via Cloudflare dashboard and should override these.
- If the rate limiter binding is unavailable (e.g. local dev), requests are **allowed** by default (fail-open)
- On rate limiter errors, requests are also **allowed** (fail-open)
- The legacy `MCP_RATE_LIMITER` binding is still used as a fallback in code but is **not present in `wrangler.jsonc`** — if not deployed in prod, both layers transparently fall back to allow-all

### Rate Limit Config (`wrangler.jsonc` — prod and canary use separate namespace IDs but identical limits)

| Binding | Limit | Period | Scope |
|---|---|---|---|
| `MCP_IP_RATE_LIMITER` | 300 req | 60s | per IP |
| `MCP_USER_RATE_LIMITER` | 60 req | 60s | per Sentry user ID |
| `CHAT_RATE_LIMITER` | 10 req | 60s | chat routes |
| `SEARCH_RATE_LIMITER` | 20 req | 60s | search routes |

## Why Cursor Automations Are Likely Hitting This

Cursor Automations can spawn **multiple concurrent runs**, each of which fires multiple MCP tool calls. All runs from the same authenticated Sentry user share the same `MCP_USER_RATE_LIMITER` bucket (keyed by Sentry user ID). At **60 req/60s**, a user running 3–4 parallel automation runs with moderate tool use can saturate the limit within seconds. Cloudflare's rate limits are **fixed-window**, not sliding — this means burst behavior at window boundaries can make it feel more aggressive than the numbers suggest.

The IP-based limit (300 req/60s) is less likely to be the culprit for single-user scenarios but could be a factor for shared corporate egress (NAT/proxy).

## Sentry Instrumentation Gap

**There are currently no Sentry events or metrics emitted when a rate limit is hit.** The 429 response is returned directly with no `captureException`, `captureEvent`, or custom metric. The `sentryBeforeSend` hook only handles scrubbing + fingerprinting — it does not add rate limit visibility. Cloudflare's built-in observability (`observability.enabled: true`) may capture request-level data, but we have no Sentry-side signal to alert on, trend, or correlate with user reports.

**We should add Sentry instrumentation at both rate limit check points** to track:
- Which limit was hit (IP vs user)
- The rate-limited identifier (hashed, already done in the key)
- Frequency / volume of 429s over time

## Investigation Still Needed

- [ ] Confirm the actual Cloudflare namespace IDs for prod bindings — the wrangler.jsonc values are dev mock IDs
- [ ] Verify whether `MCP_USER_RATE_LIMITER` and `MCP_IP_RATE_LIMITER` are actually deployed in the Cloudflare dashboard for prod (if not, the fallback to `MCP_RATE_LIMITER` would make the limits undefined/allow-all)
- [ ] Add Sentry instrumentation (metric or event) when rate limits are hit — at minimum log with `console.warn` so it's captured via `consoleLoggingIntegration`, ideally a custom metric or breadcrumb
- [ ] Evaluate whether 60 req/60s is the right threshold for agentic/automation use cases — this was sized for interactive human usage
- [ ] Consider whether automation clients should get a higher limit or a separate bucket (e.g. keyed by `clientId + userId`, or elevated for `?agent=1` requests — the query param already exists)
- [ ] Document rate limits publicly so users understand the constraints

## Expected Behavior

Users should not hit rate limits during normal Cursor Automation usage. If limits are intentional, they should be clearly documented and provide actionable guidance in the error response.

## Workaround

Reduce number of parallel automation runs; retry after a short delay.

_Action taken on behalf of David Cramer._

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.