Skip to content

fix(mcp): serve streamable HTTP statelessly to stop per-connection Durable Object writes - #270

Merged
nemolize merged 2 commits into
mainfrom
oneoff-rows
Nov 5, 2026
Merged

nemolize merged 2 commits into
mainfrom
oneoff-rows

Conversation

@nemolize

@nemolize nemolize commented Nov 5, 2026

Copy link
Copy Markdown
Owner

What happened

The Durable Objects free tier (100,000 rows_written per day) was exceeded on 2026-11-05 UTC, and writes began failing with Exceeded allowed rows written in Durable Objects free tier. Traffic was a steady 500-600 requests/min of GET/POST /sse from Claude.ai's connector.

Cause

Every GET /sse opens a new MyMCP Durable Object, and the agents library creates its bookkeeping tables on each new object. Measured in the Workers test pool, one new GET /sse costs 54 SQL statements writing 50 rows, plus 6 storage.put calls.

Fix

  • /mcp and POST /sse now run without a Durable Object: a fresh MCP server per request, no Mcp-Session-Id. No tool sends server-initiated messages, so nothing depends on a session.
  • A streamable client's follow-up GET /sse (it carries mcp-protocol-version) and DELETE get 405 instead of reaching a Durable Object. GET /mcp also gets 405.
  • The legacy SSE handshake (GET /sse without that header, then POST /sse/message) is unchanged and still uses a Durable Object.

Result

  • POST /mcp and POST /sse (initialize, tools/list, tool calls) construct 0 Durable Objects; a new E2E test asserts this, with a legacy GET /sse as a positive control that constructs exactly 1. The full suite passes.
  • Not deployed, so production rows_written is not measured yet.
  • Trade-offs: a client that only speaks legacy SSE still costs one Durable Object per connection, and its reconnect after a dropped stream now gets 405 (its headers match the streamable follow-up GET). Both are stated in the README.

Test plan

  • After deploy, compare rows_written per request via the GraphQL durableObjectsPeriodicGroups sum.rowsWritten.
  • After deploy, check cpuTime for initialize, tools/list and tools/call in Workers Observability: each request now registers every tool, and a local Node benchmark put tools/list near 8-14 ms against the Free plan's 10 ms limit.
  • Confirm the Claude.ai connector still connects and lists tools.

@nemolize nemolize self-assigned this Nov 5, 2026
@nemolize
nemolize merged commit 2e7205e into main Nov 5, 2026
4 checks passed
@nemolize
nemolize deleted the oneoff-rows branch November 5, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant