Run WebSockets, Background Workers and Cron on Fly.io With No Duration Cap

Run WebSockets, Background Workers and Cron on Fly.io With No Duration Cap

Author
Daniel Botha
Daniel Botha
Writer at Fly.io
Published

WebSockets, background workers and cron jobs all need a process that keeps running after the request ends, which is why function platforms cap them with a maximum duration and a long-running server on Fly.io does not.

The frontend deploys in a minute. Then the product needs a live feed that stays open while the tab does, a webhook starts a ten-minute import, and every night at 3am stale sessions need pruning. None of these fit a function that is invoked, answers and exits.


Can Vercel Run WebSockets, Background Workers and Long Requests?

Yes, with ceilings: Vercel Functions now accept WebSocket connections, but a connection closes when its function reaches its maximum duration, which is 300 seconds on Hobby and 800 seconds on Pro.[1][2] Background workers run as Vercel Daemons, which are in public beta.[3] For a backend whose sockets and workers run for hours, a long-running server process on Fly.io holds both without a duration cap.

What changed in 2026

WebSocket support on Vercel reached general availability in October 2026, after a public beta that began in June.[4] Function durations default to 300 seconds on every plan; Pro and Enterprise can raise that to 800 seconds, and an 1,800-second extended maximum is in beta for specific runtime versions.[2] At that limit the function returns a 504 and its WebSockets close.

Daemons run long-lived processes from a container image, such as Celery, Sidekiq or BullMQ consumers. During the beta they run only in production deployments, with instance counts set by hand, ephemeral storage only, and no autoscaling.[3]

The limits side by side

Vercel Fly.io
WebSocket lifetime Closes at function max duration: 300s Hobby, 800s Pro, 1,800s beta No proxy idle timeout; open until the app or client closes it
Long HTTP request 504 at max duration Handled by a long-running server process, with no per-invocation cap
Request and response body 4.5 MB each; streamed responses exempt from the response limit Set by your server
Background worker Daemons (beta): production only, manual instance counts, no autoscaling A [processes] group in fly.toml, scaled per group
Cron Cron Jobs: once per day on Hobby, once per minute on Pro; Schedules (beta): every 15 minutes on Hobby Supercronic process group, Cron Manager, or scheduled Machines
Open sockets per instance 1,024 file descriptors shared across concurrent executions Set by your concurrency limits and Machine size

Vercel figures from its own documentation[2][1][3][5][6], checked 2026-11-09.

The platform-level comparison is in Fly.io vs Vercel, and why functions carry these limits at all is in how serverless hosting scales.


How Long Can a Request or WebSocket Stay Open?

A WebSocket stays open until something closes it, and that something is almost never the protocol. Proxies, load balancers, corporate networks and mobile carriers close it when it has been silent for a while.

Who closes the connection

Fly Proxy does not close idle WebSocket connections: a socket with no traffic stays open until the app or the client closes it. The WebSocket docs list what does close one: a Fly Proxy restart, an autostop that stops the Machine, a deploy of the app, or a network outside Fly.io dropping it. When the proxy closes a socket for a restart or a stopping Machine, it waits for the current message to finish, then sends close code 1012 (Service Restart) to the client and 1001 (Going Away) to the app.

Long HTTP requests follow the same logic: the server process holds the request, so a long streamed response runs as long as the process and the client keep it going. The client or an upstream proxy usually gives up first, so work that takes minutes belongs in a worker, with the request returning a job ID.

Heartbeats and reconnects

Send a heartbeat, usually a ping every 30 seconds or so, so that intermediate networks keep treating the connection as live and each side notices when the other has gone. Socket.IO does this by default: the server pings every 25 seconds and treats a client that has not answered within a further 20 seconds as gone.[7] The Node.js ws library leaves it to the application, and its documentation shows the pattern: ping each client on an interval and terminate the ones that missed the last pong.[8]

Then reconnect on close, with a random delay so clients do not all return at once:

function connect() {
  const ws = new WebSocket("wss://my-app.fly.dev/socket");
  ws.addEventListener("close", (event) => {
    const delay = event.code === 1012 ? Math.random() * 1000 : 1000 + Math.random() * 4000;
    setTimeout(connect, delay);
  });
  return ws;
}
connect();

A reconnect can land on a different server, so presence, rooms and anything else shared between clients belong in Redis or Postgres rather than in process memory.


How Many WebSocket Connections Can One Machine Handle?

As many as its memory and your code allow: Fly.io sets no fixed per-Machine cap on connections, and the number Fly Proxy routes to each Machine is set by the concurrency limits in fly.toml.

Soft and hard limits

Concurrency settings apply per Machine and per service. Below the soft_limit, a Machine takes traffic normally. Between the soft and hard limits, the proxy deprioritizes it, sending new connections elsewhere unless every Machine has passed its soft limit. At the hard_limit, no new traffic reaches it. The type is either requests, recommended for plain HTTP because the proxy can pool connections, or connections, which counts TCP connections and is the right measure when sockets are the load.

The defaults matter for WebSocket apps: with nothing set, the soft limit is 20 and there is no hard limit, per the concurrency guidelines. With a soft limit of 20, Fly Proxy treats a Machine holding 21 idle sockets as loaded and routes new connections to other Machines, starting stopped ones to take them. Benchmark one Machine, find the point where latency degrades, then set the hard limit below it and the soft limit well below that, leaving a gap wide enough that connections do not oscillate across the threshold.

Where the real ceiling is

The ceiling is per-connection memory multiplied by connection count: a socket holding a small session costs little, one buffering a large document costs much more. Function platforms add a different ceiling: on Vercel, one instance shares 1,024 file descriptors across all its concurrent executions, and every socket uses one.[2]


How Do You Run a Web Server and a Background Worker Together?

Run them as two processes from the same code, each in its own Machine, so a slow job never steals CPU from a request. On Fly.io, the [processes] section of fly.toml names each process group and the command it starts. Every group runs in its own Machines from the same image, deploys together on fly deploy, and scales on its own. The background workers guide covers queue choice and retries; the config is the short part.

A worked fly.toml

app = "my-app"
primary_region = "ord"

[processes]
  web = "node server.js"
  worker = "node worker.js"
  cron = "supercronic /app/crontab"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 1
  processes = ["web"]

  [http_service.concurrency]
    type = "connections"
    soft_limit = 1000
    hard_limit = 1500

The processes = ["web"] line attaches the public HTTP service to the web group only. The worker and cron groups get no service, so the proxy never routes traffic to them, and autostop, which is a setting on a service, never stops them. Replace the concurrency numbers with your own benchmark.

Then scale each group:

fly deploy
fly scale count web=2 worker=3 cron=1

Sizing groups separately

Because each group runs on its own Machines, it can be sized for its job: fly scale vm performance-2x --process-group worker gives an image-processing worker dedicated CPU while the web group stays on shared CPU. The worker reaches the database and Redis over the app’s private network and needs no inbound port.


How Do You Run Cron Jobs on a Server That Has No Crontab?

Add a scheduler as its own process: a crontab-compatible runner in a dedicated process group, scaled to exactly one Machine. Container images usually lack a cron daemon, and classic cron behaves badly inside one: it purges environment variables before each job, which removes the secrets the job needs, and it captures job output rather than writing it to stdout.[9] Supercronic, a crontab-compatible runner built for containers, keeps the environment and logs to stdout.

The copy-paste default: Supercronic

Put a crontab next to the Dockerfile:

0 3 * * * node scripts/prune-sessions.js
*/15 * * * * node scripts/sync-billing.js

Install a pinned Supercronic release in the Dockerfile, checking its SHA1, and copy the crontab to the path the cron group reads. These lines come from the release notes for v0.2.51 on linux-amd64 and need curl in the image:[10]

ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.51/supercronic-linux-amd64 \
    SUPERCRONIC_SHA1SUM=1692066e350a88ff4d7d6474fe3c7a6c41ec5e89 \
    SUPERCRONIC=supercronic-linux-amd64

RUN curl -fsSLO "$SUPERCRONIC_URL" \
 && echo "${SUPERCRONIC_SHA1SUM}  ${SUPERCRONIC}" | sha1sum -c - \
 && chmod +x "$SUPERCRONIC" \
 && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
 && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic

COPY crontab /app/crontab

The cron = "supercronic /app/crontab" group in the fly.toml above then runs it. Scale it to one with fly scale count cron=1. One is the important number: two cron Machines run every job twice, and a digest email goes out twice.

When a job needs its own Machine

Supercronic runs jobs inside one long-running Machine, so a heavy job shares that Machine’s memory with every other job. For jobs that need isolation or more resources, Cron Manager is a small Fly app that reads a schedules.json from Git and boots a fresh Machine for each run, with its own image, size and region, then destroys it when the command exits. For work where roughly once a day is precise enough, a Machine can carry a built-in schedule of hourly, daily, weekly or monthly. The task scheduling guide compares all three.

Jobs should be idempotent either way. A deploy restarts the cron Machine and cuts off any job that is running, so write each job to pick up where an interrupted run left off.


WebSockets, Workers and Cron on Fly.io

Fly.io runs each part of this backend as an ordinary process in a Fly Machine, a hardware-isolated microVM. The web server, worker and scheduler come from one image and one fly.toml, which fly launch writes after detecting the framework, deploy together with fly deploy, and scale per group.

Because the server is a long-running process rather than a function invocation, there is no maximum duration to design around. Fly Proxy keeps idle WebSockets open, closes them only on restarts, stopped Machines and deploys, and signals those closes with code 1012 so clients know to reconnect. Autostop stops web Machines when traffic drops while leaving worker and cron groups running.

Managed Postgres holds the job tables, and Upstash Redis, created with fly redis create, carries queues and pub/sub across web Machines. A frontend can stay where it is, as in Next.js hosting, and point at the backend on Fly.io; Fly.io vs Render compares the process-based hosts.


Frequently Asked Questions

Does Fly.io close idle WebSocket connections?

No. Fly Proxy does not close a WebSocket for being idle, so a silent connection stays open until the app or the client closes it. Restarts, autostop and deploys still close connections, so clients should send a heartbeat and reconnect.

Does Fly.io have a built-in cron?

Yes. A Fly Machine can carry a built-in hourly, daily, weekly or monthly schedule. For exact cron expressions, run Supercronic in a dedicated cron process group on Fly.io, or Cron Manager, which boots a fresh Machine per run.

What happens to open WebSockets when I deploy?

They close. A deploy on Fly.io restarts each Machine with the new release, ending its connections. Clients that reconnect with a short random delay come back on the new release.

Do WebSockets work with autostop?

Yes, with reconnects. On Fly.io, before Fly Proxy autostops a Machine, it closes that Machine’s WebSockets with code 1012, and reconnecting clients reach a running Machine. Setting autostopmachines to off prevents those closures.

How do I keep only one copy of a cron job running?

Scale the cron process group to one Machine with fly scale count cron=1. Process groups on Fly.io scale separately, so web and worker can run many Machines while the scheduler runs once.

Can a worker scale separately from the web process?

Yes. On Fly.io, each process group in fly.toml runs on its own Machines, so fly scale count web=2 worker=5 sets the counts independently, and the worker can use a different Machine size.

Is there a maximum request duration on Fly.io?

No per-invocation limit applies, because Fly.io runs the server as a long-running process in a Machine rather than as a function with a timeout. Work that takes minutes still belongs in a background worker.

Do I need Redis for WebSockets across several Machines?

No, but you need a shared store. Once a Fly.io app runs more than one Machine, a reconnecting client can land on a different one, so rooms, presence and broadcasts belong in Redis or Postgres rather than in process memory.

Can I keep my frontend on Vercel and run the backend on Fly.io?

Yes. Point the frontend’s API and WebSocket URLs at the Fly.io app’s fly.dev address or a custom domain, and allow the Vercel origin in the backend’s CORS settings and WebSocket origin check.


Sources

  1. ^ Vercel, “WebSockets”. vercel.com/docs/functions/websockets. Checked 2026-11-09.
  2. ^ Vercel, “Vercel Functions Limits”. vercel.com/docs/functions/limitations. Checked 2026-11-09.
  3. ^ Vercel, “Daemons”. vercel.com/docs/daemons. Checked 2026-11-09.
  4. ^ Vercel, “WebSocket support is now generally available”. vercel.com/changelog/websocket-support-is-now-generally-available. Checked 2026-11-09.
  5. ^ Vercel, “Usage & Pricing for Cron Jobs”. vercel.com/docs/schedules/cron-jobs/usage-and-pricing. Checked 2026-11-09.
  6. ^ Vercel, “Schedules Limits”. vercel.com/docs/schedules/limits. Checked 2026-11-09.
  7. ^ Socket.IO, “Server options”. socket.io/docs/v4/server-options. Checked 2026-11-09.
  8. ^ ws, “How to detect and close broken connections?” in the README. github.com/websockets/ws. Checked 2026-11-09.
  9. ^ Aptible, “Supercronic” README. github.com/aptible/supercronic. Checked 2026-11-09.
  10. ^ Aptible, “Supercronic v0.2.51” release notes. github.com/aptible/supercronic/releases/tag/v0.2.51. Checked 2026-11-09.

Run the whole backend

Web, worker and cron process groups from one fly.toml, deployed with one command.

Sign up