Your Worker Doesn't Fit in a Serverless Function — and Never Will

·5 min read·Updated
serverlessvercelworkersbotsarchitecture

If you've tried to run a Discord bot, a queue consumer, or a polling worker as a serverless function, you've hit something that isn't a configuration problem: a function runs in response to an invocation, and a worker's defining feature is that nothing invokes it. A gateway bot holds an outbound WebSocket and waits; there's no HTTP request to trigger a function, and no duration setting that fixes it. Raising the timeout doesn't help, because "forever" isn't a bigger number. The good news is that you don't have to abandon your serverless setup — the answer is usually a hybrid: keep the request-handling where it is, and move the one always-on piece to a container.

On this platform, that always-on container is what you get by default: it runs until you stop it, with no idle policy and no session limit, for as long as there is credit on your balance.

The mismatch is structural, not numeric

It's tempting to read this as a limits problem, so it's worth separating the two.

Duration limits are real. On Vercel, functions cap at 300 seconds on Hobby — that's the default and the maximum — with Pro reaching 800 seconds and an extended 1800-second maximum in beta. Exceed it and you get a 504 FUNCTION_INVOCATION_TIMEOUT (Vercel docs). Vercel is straightforward about the boundary too, pointing workloads that need unlimited execution time at a different product entirely.

But the deeper issue isn't duration. Even thirty minutes doesn't run a bot, because a bot isn't a long request — it's a process that must exist continuously and be ready when Discord sends an event down a socket it opened. There's no invocation to attach a function to. That's why the fix isn't a bigger timeout.

Two related consequences fall out of the same model:

  • No shared in-memory state. Each invocation may run on a fresh instance, so a cache, a rate-limit counter, or a scheduler tick doesn't persist between calls. Anything stateful has to go to an external store.
  • Cold starts cost latency. For sporadic traffic the first request after idle pays for a boot — which matters when you're working against Discord's 3-second interaction deadline.

What functions are genuinely good at

This isn't an argument against serverless, and pretending otherwise would be dishonest. Functions are excellent for:

  • HTTP request/response. An API route, a form handler, an SSR page — invoked, does work, returns.
  • Webhooks, with the caveat that you should acknowledge quickly and do slow work elsewhere — the same ack-fast pattern applies regardless of platform.
  • Scheduled jobs that finish comfortably inside the duration cap.
  • Bursty traffic, where auto-scaling and paying per invocation genuinely beats a reserved container.

If your workload is one of those, functions are the right tool and a container would be a step backwards.

The hybrid that actually works

Most projects that hit this wall don't need to migrate — they need to split. Keep the parts that fit where they are, and move the one part that doesn't:

Stays serverless: your frontend, API routes, auth callbacks, webhook receivers, and anything triggered by a request.

Moves to a container: the bot, the queue consumer, the poller — anything that must be running when nothing is calling it.

They talk through whatever they already share: a database, a queue, or plain HTTPS calls. There's no special integration to build, because the worker was always going to need a data store anyway.

Concretely, a Discord bot with a web dashboard becomes two things: the dashboard on Vercel, and the bot as a worker with no exposed port — which is as small as hosted processes get.

What changes when you move that piece

Four differences worth expecting, none of them large:

  • You keep state in memory again. A single long-lived process can hold a cache or a counter — though not across a restart, so anything durable still belongs on a volume or in a database.
  • Schedules live in your process. On a continuously running app, an in-process scheduler is simple and predictable.
  • You allocate rather than consume. A container is reserved, so it costs the same idle or busy — predictable rather than metered.
  • Payload limits go away. Vercel caps request and response bodies at 4.5 MB; a container has no equivalent constraint, which matters if you're handling uploads.

Signs you've outgrown the function model

You're probably in this territory if any of these are true:

  • You're using an external service purely to poke a function on a schedule so it looks alive.
  • You've raised the duration limit more than once and are eyeing the next tier.
  • You maintain a queue, a cache, or a database purely to pass state between invocations that a single process would hold naturally.
  • Something in your system must respond to an event on a connection you opened, not a request someone sent you.

That last one is the definitive test. Inbound request means function; outbound connection means process.

FAQ

Can I run a Discord bot on a serverless function?

Not a gateway bot. Functions run when invoked, and a gateway bot holds an outbound connection that nothing invokes. Interaction-only bots receiving HTTP webhooks from Discord are a different case and can work.

Will a longer timeout fix it?

No. A worker needs to run continuously, so no finite duration is enough — the model is wrong, not the number.

What's the maximum function duration?

On Vercel, 300 seconds on Hobby (default and maximum), up to 800 seconds on Pro, with an extended 1800-second maximum in beta.

Do I have to move my whole app?

No, and usually you shouldn't. Keep request-handling serverless and move only the always-on piece to a container.

How do the two halves talk?

Through whatever they already share — a database, a queue, or HTTPS calls. No special integration is needed.

Is a container more expensive?

Container usage is charged differently: reserved rather than metered, so it costs the same idle or busy.

Wrapping up

Serverless made a genuine trade: give up persistent processes, get scaling and per-use billing. That's a good trade for request-shaped work and a bad one for anything that has to be awake when nobody is asking. The mistake isn't choosing serverless — it's assuming one execution model has to cover everything you run. Put the request handlers where requests are cheap, put the always-on process where always-on is cheap, and let them share a database.