I run a little markdown editor on the side, markpad.md. One of the things it has is an MCP server, so you can tell Claude Code "write this plan into markpad" and it comes back with a link you can send to someone. The MCP endpoint is just a Next.js route handler, deployed on Vercel's free Hobby plan. Nothing fancy.
For about a week it was doing something I had no idea about: roughly one call in seven was running for exactly five minutes and then getting killed.
I didn't see an error. The logs said 200. I only noticed because I was building a boring little admin page that pulls usage numbers from each service I depend on (Vercel, Neon, R2, the email providers), and the Vercel numbers didn't make sense.
The numbers
Vercel has a /v2/usage endpoint that gives you daily counts. First week of October, this one project:
| What | First week of October |
|---|---|
| Function invocations | ~4,000 |
| Invocations killed at the 300 s timeout | 570 (about 1 in 7) |
| Memory time, calls that finished | 1.0 GB-hours |
| Memory time, calls that got killed | 97.3 GB-hours |
So 99% of my function time was going to requests that produced nothing.
Hobby includes 360 GB-hours a month. I was a week in and nearly a third of the way through it. And on Hobby, going over doesn't mean a bill. Vercel pauses every project on your account for 30 days. I would have found out when the site went dark.
Once I knew what to look for, the logs did have it. Vercel writes this when it kills a function:
Vercel Runtime Timeout Error: Task timed out after 300 seconds
Every single one was POST /mcp. With status 200. Which is why filtering the logs by "error" had shown me nothing for a week.
What was holding the function open
The MCP server uses createMcpHandler from @modelcontextprotocol/server (v2), stateless, new instance per request, the standard serverless setup.
Clients on the newer protocol revision (2026-07-28) send a subscriptions/listen request. The idea is the server can push "hey, my tool list changed" to the client later. The SDK answers that with an SSE stream, sends a keepalive comment every 15 seconds, and keeps it open until the client goes away.
On a normal server, fine. On a serverless function, "until the client goes away" means "until the platform kills me at the max duration". Five minutes, billed, for a stream that was never going to carry anything. Markpad's tools don't change.
I'll be honest about the detective work: Vercel's logs don't show request bodies, so I never actually saw a subscriptions/listen call. I read the docs for the SDK's maxSubscriptions option, which describe exactly this stream, thought "that's it", and the fix confirmed it.
Claude Code opens one of these per session, as far as I can tell. So every person who connected their editor was costing me five minutes of compute every time they sat down to work. The actual tool calls, the ones that do something, take milliseconds.
The fix is one line
const mcp = createMcpHandler(() => createNotesServer(), {
legacy: 'stateless',
// Our tools never change, so there's nothing to subscribe to. A listen
// stream on a serverless function stays open until the platform kills it.
maxSubscriptions: 0,
});
With maxSubscriptions: 0 the SDK refuses the listen request straight away, in-band, with a JSON-RPC error (-32603 Subscription limit reached). The client shrugs and carries on. Tool calls work exactly as before.
Since deploying it: zero timeouts in the first couple of hundred calls, and the memory-time line on my usage page stopped climbing.
If your tool list genuinely changes at runtime, this isn't your fix. You'd want a real event bus (the handler takes a bus option) and somewhere that can hold a connection open, or you accept that the streams cost money.
What I'd do differently next time
Two things, both obvious in hindsight.
Set maxDuration on the route. A Next.js route handler can export it. Nothing in my MCP server has any business running longer than 30 seconds, so a held-open stream should cost 30 seconds, not 300, while I work out why it's held.
And read the usage API, not just the error log. The killed requests were logged as successes. The only place this problem was visible was the money. The usage page took me an afternoon, and it's the only reason I caught this a week in instead of a month in, with the site paused.
The app
If you want to poke at it: markpad.md. Open a tab, write markdown, see it rendered, share a link. No account needed to write, sign in when you want to share. The MCP server is at api.markpad.md/mcp and it's free. If you hook Claude Code up to it and see something weird in your own function logs, tell me, I'd rather hear it from you than from the usage page.
Top comments (0)