Top 7 Celery Hosting Platforms in 2026
A Celery worker is a process that starts, connects to a broker, and then sits there for weeks. That single sentence eliminates half the places people try to put one. Serverless functions time out. Request-driven autoscalers see no inbound HTTP and scale you to nothing. Platforms with one process per service make you buy a second service before you have a second machine's worth of work.
I build PandaStack, so read our entry as an interested party talking. No prices below: they move, and this post will not.
What Celery actually needs from a host
People shop for Celery hosting thinking they are buying one thing. They are buying three, and the third gets forgotten until the invoice arrives.
- A long-running worker process, not a function invocation. It holds a broker connection, forks children to run tasks, and must shut down politely or in-flight work is lost.
- A broker: Redis or RabbitMQ, or SQS if you are deep in AWS. Separate infrastructure with its own durability story, and no Python host makes it disappear.
- Usually a beat scheduler. Exactly one, cluster-wide, or periodic tasks fire twice. This is what breaks the naive answer of scaling the worker to three.
- Often a result backend: Redis again, Postgres via SQLAlchemy, or nothing if you set task_ignore_result and stop asking for results you never read.
# proj/celery.py
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proj.settings")
app = Celery("proj")
app.conf.update(
# Broker: Redis here. RabbitMQ would be amqp://user:pass@host:5672//
broker_url=os.environ["CELERY_BROKER_URL"],
# Result backend is optional. Postgres works if you actually read results;
# if you don't, set task_ignore_result=True and skip the backend entirely.
result_backend=os.environ.get("CELERY_RESULT_BACKEND"),
task_ignore_result=os.environ.get("CELERY_RESULT_BACKEND") is None,
# Ack late so a killed worker's task goes back on the queue instead of vanishing.
task_acks_late=True,
worker_prefetch_multiplier=1,
# Redis has no native ack timeout; this is how long a task may run before
# the broker assumes the worker died and redelivers it. Set it above your
# slowest task or you will run that task twice.
broker_transport_options={"visibility_timeout": 3600},
task_time_limit=1800,
task_soft_time_limit=1500,
timezone="UTC",
)
app.autodiscover_tasks()That visibility_timeout line is the most common production surprise with a Redis broker. Redis has no real acknowledgements, so Celery emulates them with a timer: a task that runs longer gets redelivered while the first worker is still going, and you get a duplicate.
# The worker. --concurrency tracks CPU for CPU-bound tasks; for IO-bound work,
# raise it or switch to the gevent pool.
celery -A proj worker \
--loglevel=INFO \
--concurrency=4 \
--max-tasks-per-child=200 \
--hostname=worker@%h
# Beat, separately. EXACTLY ONE of these across the whole fleet.
celery -A proj beat --loglevel=INFO --schedule=/var/lib/celery/beat-schedule
# Or, for small deployments, embed beat in a single worker with -B.
# Convenient, and a trap the moment you scale that worker to 2 replicas.
celery -A proj worker -B --loglevel=INFO --concurrency=4
# Sanity checks that answer "is anything actually consuming?"
celery -A proj inspect ping
celery -A proj inspect activeThe broker is its own decision, everywhere
None of these platforms hosts Celery. They host a Linux process that happens to be a Celery worker. What differs is the broker story, the process model, and whether idle costs money. Redis is the default because it is trivial and fast; RabbitMQ is better if you want real acknowledgements, priorities and dead-letter routing. Decide early whether it is managed or yours: retrofitting durability onto a Redis you installed on the app box is a bad afternoon.
The seven options
1. Heroku dynos
The original answer and still a clean one. A Procfile with web, worker and beat entries maps one-to-one onto Celery's process model, and add-ons give you managed Redis or RabbitMQ in the same account. You pay for that predictability: three always-on dyno types plus the add-on. Dynos restart daily, so workers must handle SIGTERM gracefully, which with acks_late they do.
2. Render background workers
Render has a first-class Background Worker service type, exactly the primitive Celery wants: a long-running process with no HTTP port and no health check expecting a response. Web, worker and beat become three services in render.yaml alongside a Redis instance, in one blueprint. The most direct mapping of Celery's topology onto a PaaS, and each service is its own always-on instance.
3. Railway
One project, several services from the same repo, each with its own start command, plus one-click Redis and Postgres over private networking. Adding Celery is: duplicate the service, change the start command, done. The fastest of these from zero to a running worker.
4. Fly.io machines
Fly gives you individually addressable Firecracker VMs, with process groups in fly.toml so one app definition runs a web group and a worker group from the same image. Machines start and stop by API in about a second, so a queue-depth autoscaler is genuinely buildable. You own more of the supervision than on Render.
5. AWS ECS on Fargate
For teams whose infrastructure is already IaC and whose broker is already ElastiCache, Amazon MQ or SQS. One task definition per role, autoscaling on a CloudWatch queue-depth metric, IAM roles instead of env-var credentials, no host to patch. More setup than the rest, and it scales furthest without a rethink. Beat gets its own service pinned to one replica.
6. A plain VM with systemd
Underrated. One VM running your web server, a worker unit, a beat unit and a local Redis carries a startup a long way at a cost you can predict to the cent. systemd gives you restarts, log capture and ordering for free. The costs are boring: you patch it, you monitor it, and if Redis is on that box the queue dies with it.
7. PandaStack
Straight version first: we do not host managed Redis or RabbitMQ. Your broker is a separate decision, a managed provider or one you run on a persistent sandbox. Our managed database is Postgres 16, a fine Celery result backend via SQLAlchemy and a bad broker. If you want one vendor selling Celery plus its broker, that is Heroku or Railway, not us.
What we offer is a full Firecracker microVM per app with a normal Ubuntu userland and root, not a container slot with a single-process contract. Run gunicorn and celery worker in the same app from one deploy and one bill, or split the worker out when it needs its own CPU. Billing is $0.054 per vCPU-hour and $0.0162 per GiB-hour, with no per-request charge.
Running web and worker in one instance
Same shape on a VM, a Fly machine or a PandaStack app, and it saves a service.
#!/usr/bin/env bash
# Start command: web + worker + beat in one instance.
set -euo pipefail
export DJANGO_SETTINGS_MODULE=proj.settings
mkdir -p /var/lib/celery /var/log/celery
# Worker in the background, logs to a file you can tail.
celery -A proj worker --loglevel=INFO --concurrency=2 \
>>/var/log/celery/worker.log 2>&1 &
WORKER_PID=$!
# Beat too, since this instance is a singleton by construction.
celery -A proj beat --loglevel=INFO \
--schedule=/var/lib/celery/beat-schedule \
>>/var/log/celery/beat.log 2>&1 &
BEAT_PID=$!
# If either background process dies, take the whole instance down so the
# platform restarts it. A web server serving 200s with a dead worker is worse
# than a restart, because nothing alerts on it.
trap 'kill $WORKER_PID $BEAT_PID 2>/dev/null' EXIT
exec gunicorn proj.wsgi:application \
--bind "0.0.0.0:${PORT:-8000}" --workers 3 --timeout 60Two rules make this safe. Give the instance enough RAM for both: the prefork pool forks one child per concurrency slot, each carrying a full copy of your app's imports. And never scale past one replica while beat is embedded.
Why scale-to-zero is the wrong feature here
PandaStack apps hibernate when idle and wake in about 1.2 seconds. For a web app that is close to free money. For a Celery worker it is actively wrong, and I would rather say so than have you learn it from a stalled queue.
The mechanism is the problem. Idle detection is driven by inbound HTTP: no requests means nobody needs this, so sleep it. A worker has no inbound HTTP at all. Its whole job is an outbound broker connection and a blocking wait, so a healthy worker chewing through a thousand tasks looks like a dead one. Nothing wakes it either: a message landing in Redis is not a request to your app.
So run workers always-on, on us or anywhere else. Co-locate the worker with a web process that receives traffic and keeps the instance warm, or give it its own always-awake app. If your volume is bursty and you want the idle savings, the right shape is not a sleeping worker but a scheduled job that starts a sandbox, drains the queue, and exits.
Choosing in five minutes
- Pick the broker first. It constrains the host list more than the reverse.
- Count your processes. Web plus a small worker plus beat is one co-located instance. A worker with its own CPU profile needs its own service.
- Decide where beat lives, once, in writing. Ambiguity here is how you send a billing email twice.
- Check the platform supports a process with no HTTP port. Request-based autoscaling needs a warm-keeping story.
- Verify current prices yourself; the count of always-on instances matters more than the per-hour rate.
The short version
Heroku or Render if you want Celery's three processes to map onto three obvious buttons. Railway for the same in ten minutes. Fly if you want machines you start and stop against queue depth. ECS on Fargate if your infrastructure is already AWS-shaped. A plain VM if you want to know exactly what it costs. And PandaStack if a real Linux microVM, web and worker in one deploy, is worth bringing your own broker for.
Frequently asked questions
Can I run a Celery worker on a serverless function?
Not as a worker, no. A Celery worker is a long-lived process that holds a broker connection and blocks waiting for messages, while a serverless function is invoked, runs, and exits under a timeout. You can invert the pattern — have something push work to a function per message, using SQS to Lambda for example — but then you are not really running Celery, you are running a message-triggered function, and Celery's routing, chords, retries and result semantics stop applying. If you want Celery itself, you need a host that runs a process indefinitely.
Does PandaStack provide a Redis or RabbitMQ broker for Celery?
No. We host managed Postgres 16, not managed Redis or RabbitMQ, so your Celery broker is a separate decision: a managed Redis provider, or a Redis or RabbitMQ you run yourself on a persistent sandbox. Postgres works fine as a Celery result backend through SQLAlchemy, but it is not a good broker. What we do give you is a full Linux microVM per app, so the web process and the Celery worker can run in the same instance from one deploy rather than as two separately billed services.
Should the Celery worker run in the same instance as the web process?
It depends on the worker's CPU profile. Co-locating is cheaper and simpler while tasks are small and bursty: one start command launches gunicorn plus a worker, and web traffic keeps the instance warm. It stops being a good idea when worker CPU competes with request latency — a batch of image resizes will starve your web server on a shared box. Keep RAM in mind too, because Celery's prefork pool forks a full copy of your app per concurrency slot. Split the worker out the first time a task makes requests slow.
Why does scale-to-zero break Celery workers?
Because idle detection is based on inbound HTTP requests and a Celery worker never receives any. Its work arrives over an outbound connection to the broker, so a worker busily draining a thousand tasks looks identical to a dead one from the platform's perspective, and it gets hibernated. Nothing wakes it either — a message arriving in Redis is not an HTTP request to your app, so there is no wake trigger. Run workers always-on, co-locate them with a web process that receives traffic, or use scheduled jobs that spin up, drain and exit.
How many Celery beat processes should I run?
Exactly one, cluster-wide. Beat is the scheduler that publishes periodic tasks, so a second instance means every periodic task fires twice: duplicate billing runs, duplicate emails, duplicate reports. The convenient shortcut is embedding beat in a worker with the -B flag, which is fine for a single non-replicated instance and becomes a bug the moment you scale that service to two. For anything replicated, run beat as its own service pinned to one replica, use a locking scheduler backend, or replace it with your platform's own scheduler.
Redis or RabbitMQ as the Celery broker?
Redis is simpler, faster to stand up and the common default, but it has no native message acknowledgement, so Celery emulates one with a visibility_timeout. Any task running longer than that timeout gets redelivered while the original is still working, producing duplicates. RabbitMQ has real acknowledgements, priorities and dead-letter routing, and survives restarts more gracefully, at the cost of being more to operate. Start with Redis if your tasks are short and idempotent; move to RabbitMQ when correctness under redelivery matters more than setup time.
Keep reading
- Running background workers next to your web app — The general shape this post applies to Celery.
- How to run a Kafka consumer alongside your app — The same co-location question, different queue.
- The best managed Redis providers in 2026 — Pick the broker before you pick the host.
- Best Python hosting platforms in 2026 — Where the web half of the app should live.
- App hosting on PandaStack — Git-driven deploys into a full Linux microVM.
- Managed Postgres — A result backend, not a broker.
49ms p50 cold start. Fork, snapshot, and scale to zero.