· 7 min read

Vercel Functions Can Now Run 30 Minutes. That's Long Enough to Run an Agent, and Long Enough to Talk Yourself Into the Wrong Architecture.

On June 15, Vercel raised the ceiling on its Functions: Node.js and Python functions can now run up to 30 minutes, more than double the previous 800-second limit, for Pro and Enterprise teams. The changelog lists exactly the use cases you'd expect: long LLM reasoning and tool calls, AI responses that stream for minutes, document and media processing, web scraping, browser automation, queue handlers. It reads like a pure quality-of-life win. For a solo builder, it's that and a quiet invitation to make an architectural mistake you'll feel later.

Let me be fair to Vercel first, because the obvious hot take here is wrong. The naive fear is "30 minutes times the per-second rate equals a terrifying bill," and that's not how it works. Vercel's longer durations run on Fluid Compute, and active CPU billing only applies while your code is actually executing. It pauses while the function waits on I/O, which is most of what an agent does: model calls, database queries, third-party APIs. So a function that spends 28 of its 30 minutes waiting on an LLM isn't billing you for 28 minutes of compute. The pricing model is genuinely more forgiving than the wall-clock number implies. If you came here for "this will bankrupt you," it won't, and saying so would be the cheap version of this post.

So where's the actual problem

The problem isn't the per-invocation bill. It's what a 30-minute ceiling encourages you to build.

When the maximum function duration was short, the platform was enforcing an architecture for you. Anything that took real time (a long agent run, a big scrape, a batch job) had to be pushed onto a proper queue or background worker, because it physically couldn't live inside a request. That constraint was annoying and it was also doing you a favor. It kept long-running work in systems designed for long-running work: things with retries, dead-letter queues, observability, and concurrency control built in.

Raise the ceiling to 30 minutes and the constraint dissolves. Now the lazy path works. You can wire an agent loop directly into a function, let it churn for twenty minutes, and ship it. It'll run. The demo will be clean. And you've just put a long-lived, stateful, retry-prone workload inside a request-response abstraction that was never meant to hold it. That's a worker queue wearing a serverless costume, and the costume comes off at the worst possible time.

What bites, specifically

A few things, and none of them show up in the happy-path demo.

Concurrency is the first. One 30-minute function is fine. The active-CPU model keeps a single mostly-waiting agent cheap. But functions scale out per request, so if a hundred users each kick off a 30-minute agent run at once, you have a hundred concurrent long-lived executions, and the compute that is active (the reasoning steps, the parsing, the tool orchestration between the I/O waits) multiplies by a hundred. The forgiving per-invocation math stops being reassuring when invocations fan out and overlap.

Retries are the second, and they're nastier inside a function than in a queue. If your 25-minute job fails at minute 24 and something upstream retries the whole request, you don't resume: you re-run from the top, paying the active CPU again and redoing the work. A real queue lets you make steps idempotent and retry the failed step. A function retrying a long opaque job just repeats the whole expensive thing. CPU-bound work makes this worse: if your long task is actually computing rather than waiting (heavy parsing, image or document processing, anything that isn't I/O), then the gentle "we pause billing during waits" promise doesn't apply, because you're not waiting, you're working, and now the 30-minute window is 30 minutes of billed CPU.

The third is observability. A request that runs for half an hour is a request you can't see inside of with normal tooling. When it hangs at minute 18, you get a timeout and a shrug, not the per-step trace a workflow engine would give you. Debugging a long opaque function is materially harder than debugging a pipeline of short steps.

What I'd actually do

Use the 30-minute ceiling for what it's genuinely good at: a single, mostly-I/O-bound task that's a little too long for the old limit and doesn't need to fan out: a one-off long LLM call, an occasional document extraction, a scheduled job that runs once and isn't user-triggered. For those, this update removes a real annoyance, and you should take it.

Set maxDuration deliberately, per function, to the smallest number that fits the job: not the maximum, and never globally. The opt-in is a feature: a function that's supposed to finish in ten seconds should be capped near ten seconds, so a runaway loop or a stuck tool call dies fast instead of grinding to the ceiling. Treating 30 minutes as the default rather than the exception is how you turn a safety valve into a foot-gun.

And when the work is actually queue-shaped (user-triggered, fan-out, retry-prone, or CPU-bound), reach for a queue or a workflow engine even though the function would technically run. The 30-minute limit means you can skip that step. It doesn't mean you should. The platform stopped enforcing the right architecture; that just means you have to enforce it on yourself now.

The honest counter-take: for a genuinely early solo project, the lazy path is often the correct path, and I don't want to dress up premature infrastructure as wisdom. If you have five users and a job that runs twice a day, wiring up a proper queue with dead-letter handling and idempotent steps is exactly the kind of over-engineering that kills momentum, and shoving it in a 30-minute function so you can ship today is the right call. The failure mode isn't using the long function early. It's never revisiting it: letting the thing you built when it ran twice a day keep running when it runs ten thousand times a day, discovering the architecture was wrong only when the retries stack up and the concurrency bill arrives. Ship the easy version. Just write down the line where it stops being the easy version, and actually watch for it.

Author

Sources

Stay in the Loop

Get new posts delivered to your inbox. No spam, unsubscribe anytime.

Newsletter coming soon. Set PUBLIC_CONVERTKIT_FORM_ID in .env to activate.

Related Posts