The Best Prefect Hosting Platforms in 2026
Search for how to host Prefect and you will find two kinds of answer that contradict each other, and the reason is that they are answering different questions. One set of posts tells you to run a container, point it at Postgres and open port 4200. The other tells you about work pools, job variables and image tags. Both are correct. Neither is complete, because hosting Prefect is not one deployment decision — it is two, they are almost independent, and confusing them is where most of the pain comes from.
Question one: where does the Prefect server live? That is the API, the UI, the background services that turn schedules into flow runs, and a database that holds all of it. It is an ordinary long-lived web service with an ordinary long-lived database, and if you have ever deployed a Django app you already know how to do this. Question two: where do flow runs actually execute? That is work pools, workers and per-run infrastructure, it is where all of your compute bill lives, it is where your isolation boundary either exists or does not, and it has nothing whatsoever to do with question one. You can run the server on Prefect Cloud and the workers on a Raspberry Pi under your desk. People do.
I'm Ajay. I build PandaStack, a Firecracker microVM platform that turns up near the bottom of this list and is not the right answer to question one at all. That gives me an unusual amount of freedom to be nice about Prefect Cloud, which for a lot of teams is simply the correct choice and I will say so more than once. Everything below about anyone else is qualitative: no invented prices, no invented latencies, no benchmarks for products I do not operate. Prefect's config keys and CLI spellings also move between minor versions, so treat every snippet as a shape and check it against the docs for the version you actually run.
The two questions, drawn properly
Here is the whole system in five boxes. Get these straight and every option later in this post is just a different opinion about who runs which box.
- The API server. A FastAPI application that every other component talks to over HTTP. Your flows report state to it, workers poll it, the UI reads from it, your laptop deploys through it. It is stateless in the useful sense — all the state is in the database — which means it is the one component you can scale and restart casually.
- The UI. Served by the same process on the same port. There is nothing to deploy separately and nothing interesting to say about it, which is a compliment.
- The background services. The scheduler that materialises future flow runs from your schedules, the service that marks a run Late when nobody picked it up, the cancellation cleanup, the automations and event triggers. These are the parts that must be running for the system to feel alive. By default they run inside the same process as the API, which is convenient right up until you scale the API to two replicas and have to find out what your version wants you to do about it.
- The database. Every flow run, task run, state transition, log line and artifact. SQLite by default, which is a development convenience and a production trap. More on that shortly, because it is the single most common self-hosted Prefect mistake.
- And then the execution layer: work pools, workers, and the process or container or pod or VM that each individual flow run happens inside. This box has no relationship to the four above other than an HTTPS connection. It is also where essentially all of your money goes.
The important structural fact is the direction of that HTTPS connection. Workers dial out to the API and poll for work. The API never connects to a worker. There is no inbound port on a worker, no NAT hole, no VPN between Prefect's control plane and your compute. A worker behind three firewalls in a private subnet behaves identically to one on a public host, and that is why the split-brain deployment — hosted control plane, your own compute — is so common and so painless. It also means that when you pick an execution platform, you only need it to make outbound HTTPS requests. That is a very low bar, and it opens up options that would be impossible for a system that needed to be dialled into.
The server half: boring on purpose, until SQLite
If you are self-hosting, the server is genuinely undramatic. One process, one port, one database, restart it whenever you like. It is a better-behaved web service than most of what your company already runs. There is exactly one landmine, and everyone steps on it.
SQLite is a development default, not a deployment choice
Prefect ships with SQLite so that a fresh install works in one command with no dependencies, and for a laptop that is exactly right. It is also entirely reasonable for a genuinely single-user, low-concurrency server that ticks over a handful of flows an hour. What it is not built for is concurrency, and Prefect's write pattern is unusually concurrent for something that looks like a metadata store: every task run transition, every log line, every heartbeat from every worker, all landing at once whenever your schedules cluster on the hour — which they do, because everyone writes cron expressions ending in zero.
The failure mode is not a crash. It is a database-is-locked error at 09:00 on the day you added the twelfth flow, followed by a UI that spins, followed by workers whose state updates time out while the flow itself is running perfectly fine. You end up debugging your orchestrator instead of your pipeline, which is the exact inversion of why you adopted an orchestrator. Use Postgres. Point at it before the first start, not after — a server that has already created a SQLite file will keep cheerfully using it while you wonder why your new database is empty.
# ---------------------------------------------------------------------------
# The server half. One process serving the API and the UI, plus the background
# services, backed by a real database. Setting names are stable-ish across
# Prefect 3.x but do check `prefect config view` for your version.
# ---------------------------------------------------------------------------
# Postgres, not SQLite. Set this BEFORE the first `prefect server start` --
# once ~/.prefect/prefect.db exists the server is perfectly happy to keep
# using it, and you will spend an afternoon wondering why the UI is empty.
export PREFECT_API_DATABASE_CONNECTION_URL=\
"postgresql+asyncpg://prefect:${PGPASSWORD}@db.internal:5432/prefect"
# Apply migrations explicitly as a deploy step. Do not let a cold start do it
# implicitly: two replicas racing to migrate the same schema is a bad first day.
prefect server database upgrade -y
# The API and the UI are the same process on the same port. Bind to 0.0.0.0 or
# nothing outside the container can reach it.
prefect server start --host 0.0.0.0 --port 4200
# THE SECURITY NOTE. A self-hosted Prefect server has historically shipped with
# no authentication at all -- anyone who can reach the port can read every log
# line, trigger every deployment and delete every flow. Recent 3.x versions
# added a basic-auth setting; whether or not yours has it, keep the server on a
# private network behind your own SSO proxy. "It's only on the VPN" is a
# perfectly good answer. "It's on a public IP with a hard-to-guess hostname"
# is not an answer, it is a countdown.
# export PREFECT_SERVER_API_AUTH_STRING="admin:$(openssl rand -hex 24)"
# Everything else -- workers, your laptop, CI -- points at that one URL. The
# /api suffix is load-bearing and omitting it produces a confusing 404 loop.
export PREFECT_API_URL="https://prefect.internal/api"Beyond that, the server is genuinely low-drama. It has no leader election to get wrong in the small case, no gRPC code servers to version-match, and — a real difference from some peers — its background services do not have to be a single fragile singleton process you nurse. Restart it during a flow run and the flow keeps running; it will reconnect and report state when the API comes back. That is a deliberate design property and it makes the server much less scary to host than the equivalent component in several other orchestrators.
Size the database above what a demo suggests. Flow run and task run rows accumulate, and log lines accumulate much faster than either, because Prefect ships logger output to the API so that you can read it in the UI. A pipeline that logs per row instead of per batch will grow the log table faster than any of your data. Check whether your version has retention controls, and if it does, turn them on before you need them rather than during the incident where the UI has become slow and nobody knows why.
Work pools and workers: the half that decides the bill
Now the interesting question. A work pool is a named queue plus a description of what infrastructure a flow run should get. A worker is a lightweight process that polls one pool, and when it finds a scheduled run, provisions that infrastructure and hands the run to it. The pool's type determines what 'that infrastructure' means, and it is a much bigger decision than the name suggests.
- Process pool — the worker runs each flow run as a subprocess on its own machine. Near-zero startup latency, zero infrastructure, and every flow run shares one filesystem, one Python environment, one set of environment variables and one kernel. Perfect for a single-VM deployment; a noisy-neighbour and dependency-conflict incident waiting to happen the moment two teams share it.
- Docker pool — one container per flow run on a Docker host. Real resource limits, real per-deployment images so the ML team's numpy pin and finance's ancient vendor SDK never meet, startup measured in low seconds plus whatever the image pull costs. The natural fit for a one-box or small-fleet deployment, and the setup I would push most teams toward before they reach for a cluster.
- Kubernetes pool — one Kubernetes Job per flow run, with per-deployment resource requests, node autoscaling and your cluster's whole scheduling apparatus behind it. Excellent for a two-hour training job. A poor trade for a forty-second flow, where pod scheduling plus image pull can genuinely exceed the work.
- Cloud container pools — ECS, Cloud Run, Azure Container Instances and friends. Same shape as Kubernetes without the cluster, using your cloud's task-startup characteristics and its IAM model. A reasonable middle ground for shops that deliberately do not run Kubernetes.
- Push pools — the serverless-ish ones, where there is no worker at all: Prefect Cloud submits the run directly to your cloud account's serverless container service. This is a genuinely appealing model because the thing you were going to leave running all night stops existing. Note the constraint clearly, because it decides shortlists: push pools are a Prefect Cloud feature, not something the open-source server does.
- Managed pools — Prefect runs the compute as well as the control plane. Nothing to deploy, nothing to keep alive. Also Cloud-only, and with the resource and duration ceilings you would expect from any hosted runtime, which are exactly the numbers you should verify against current documentation rather than mine.
The other half of the picture is how your flow code reaches the machine. Prefect gives you two broad options and they have quite different operational feels. You can bake the code into an image and reference it in the pool's job variables, which is a build pipeline per change and a reproducible artifact per run. Or you can use a pull step — most commonly a git clone that runs at flow-run time — which means deploys are a git push and there is no registry in the loop, at the cost of your flow runs now depending on your git host being up. Neither is wrong. Pick deliberately, because retrofitting the other one later touches every deployment.
# prefect.yaml -- lives at the root of your flow repo; `prefect deploy` reads
# it. Key names shift between minor versions, so verify against the docs for
# the version you actually run before copying this wholesale.
name: analytics-flows
prefect-version: "3.x"
# `pull` steps run on the WORKER side, at flow-run time. This clone therefore
# happens once per flow run -- which is fine, and is also why your git host is
# now a runtime dependency of your pipelines. If that bothers you, bake images
# instead and drop this block.
pull:
- prefect.deployments.steps.git_clone:
repository: https://github.com/acme/analytics-flows.git
branch: main
credentials: "{{ prefect.blocks.github-credentials.acme-bot }}"
deployments:
# The archetypal pipeline: two minutes of work, every hour, forever. This is
# the shape that makes an always-on worker fleet look silly on a bill.
- name: hourly-rollup
entrypoint: flows/rollup.py:hourly_rollup
work_pool:
name: docker-pool
job_variables:
image: ghcr.io/acme/analytics:2026.09
env:
WAREHOUSE_DSN: "{{ $WAREHOUSE_DSN }}"
schedules:
# Not on the hour. Everything in your company is on the hour, and the
# thundering herd lands on the same warehouse and the same database.
- cron: "7 * * * *"
# NEVER leave this implicit. A schedule with no timezone will find a
# DST boundary eventually, and it will find it on a quarter close.
timezone: Europe/London
# Same repo, same pool, wildly different resource shape. job_variables are
# per-deployment overrides on the pool's base job template, which is how one
# pool serves both of these without you running two worker fleets.
- name: nightly-backfill
entrypoint: flows/backfill.py:backfill
work_pool:
name: docker-pool
job_variables:
image: ghcr.io/acme/analytics:2026.09
mem_limit: 24g # docker-pool spelling; k8s pools use their own
schedules:
- cron: "0 3 * * *"
timezone: Europe/London
# ---------------------------------------------------------------------------
# And the pool + worker side, which is three commands and one long-lived
# process per pool you care about:
#
# prefect work-pool create docker-pool --type docker
# prefect work-pool set-concurrency-limit docker-pool 8
# prefect worker start --pool docker-pool
#
# That concurrency limit is not optional decoration. Without it, the first
# backfill over a year of daily partitions will try to open several hundred
# simultaneous connections to a warehouse that has opinions about that.
# ---------------------------------------------------------------------------The shape of the workload: bursty, long-tailed, mostly idle
Here is the thing that should actually decide your platform, and it is almost never the thing people compare on. Look honestly at your flow runs. Not the impressive one from the architecture diagram — all of them, over a week. In every Prefect deployment I have looked at, the distribution has the same two properties.
It is bursty. Schedules cluster ferociously: on the hour, at midnight, at 06:00 when the upstream extract lands. Between those spikes there are long stretches where the fleet does nothing at all. And it is long-tailed. The overwhelming majority of your runs are short — a two-minute rollup, a thirty-second freshness check, a sensor-ish poll that finds nothing and exits — while a handful of nightly backfills and model trainings run for hours and consume most of the actual CPU. The median run and the mean run are not remotely the same animal, and any capacity plan built on the mean will be wrong in both directions at once.
Now put that against how you are being asked to buy compute. An always-on worker fleet is sized for the burst, because that is the only way the burst completes on time — and then it sits at that size through every quiet hour, being paid for. The utilisation arithmetic is unkind and entirely obvious once you write it down: if your pipelines genuinely execute for two minutes an hour, a permanently-running worker is idle for something like ninety-seven percent of the wall clock, and you are paying for a machine to poll an API and wait. That is not a scandal — plenty of infrastructure works this way and is fine — but it is worth saying out loud, because it is the specific inefficiency that the newer options in this post exist to remove.
There is a real counter-pressure, and I do not want to hand-wave it. Always-on workers are fast. The subprocess starts immediately, the image is already in the local cache, and the deployment's dependencies are already installed. Any model where infrastructure is created per run pays some startup cost, and if that cost is measured in tens of seconds then for a thirty-second flow you have doubled the run time to save the idle. That is the actual trade, and it is why 'scale to zero' is not automatically the right answer. It is the right answer when the per-run startup cost is small relative to the run — which is a question about milliseconds versus minutes, and therefore a question about how the per-run machine is created.
The orchestrator question is 'what should run and when'. The hosting question is 'what am I paying for while nothing is running'. Almost every platform comparison answers the first one and quietly ignores the second.
If flows run code you did not write, isolation is a boundary, not a preference
Everything above is about resource isolation: stopping one flow run's memory appetite from starving another. There is a second question that looks similar, has a completely different answer, and is increasingly the one that matters. What if the Python inside the flow run is not Python your team wrote?
This is no longer an exotic scenario. A flow that pip-installs whatever a customer's requirements file says. A transform an analyst wrote and a model finished. A connector a partner contributed. A notebook your product lets users upload and schedule. An agent step where a language model emits code and something runs it — which is the version of this problem that has grown fastest, because the model does not have to be malicious to emit a path-traversal, an accidental credential print, or a spectacularly literal interpretation of 'clean up the working directory'.
In every one of those cases the code executes inside your flow run's process, with your flow run's credentials: the warehouse DSN, the object-storage token, the Prefect API key, the cloud instance metadata endpoint sitting one HTTP request away. A process pool means it also executes on the same machine as every other tenant's flow runs, sharing a filesystem where the previous run's temp files are still sitting.
A Docker or Kubernetes pool is a large improvement and I am not going to be sniffy about it — real resource limits, a clean filesystem per run, per-deployment dependency sets. But the boundary is namespaces and cgroups over a shared host kernel, and that boundary is a packaging mechanism that happens to have some security properties rather than a security mechanism that happens to package things. For code your team wrote, that is a completely reasonable place to stop. For code you did not write, the honest engineering position is that a container is a strongly-worded suggestion to a kernel that both parties share, and the boundary you actually want is a hypervisor.
The reason to decide this early is that it is not a knob. Moving from a shared-kernel worker to a per-run hardware boundary changes how credentials reach the run, how results come back, how logs are collected and how concurrency is capped. Retrofitting it after you have signed a customer who asked about it is a quarter of platform work, not a config change.
The hosting options, honestly
1. Prefect Cloud
The first-party managed control plane: the API, the UI, the services, the database, the upgrades and the availability of all of it are someone else's pager. You keep the interesting half — your work pools and workers, wherever you want them — or you hand that over too, via managed or push pools.
For most teams this is the correct answer to question one and I would like to be unambiguous about that. The server is not a hard thing to run, but it is a thing to run: a database to back up, a TLS certificate to renew, an SSO story to invent because the open-source server does not ship one, a version-upgrade cadence to keep up with, and a small pile of alerting that only exists because you decided to self-host. That work is real and it is not differentiated. Unless you have a compliance requirement that keeps the metadata inside your perimeter, or a genuine cost model that says otherwise at your scale, buying the control plane is the choice that lets you spend your attention on the pipelines instead.
Cloud is also where the interesting execution models live, which is worth knowing before you build a shortlist around the open-source server. Push work pools — no worker process at all, runs submitted straight into your own cloud account's serverless container service — and managed pools, where Prefect supplies the compute, are Cloud features. If the reason you were drawn to self-hosting was 'I don't want an always-on worker', it is worth checking whether the hosted product already solves exactly that. Verify current tiers, limits, RBAC and audit-log availability against Prefect's own documentation; those are precisely the specifics that decide this and precisely the ones that change.
2. Kubernetes
The maximum-control option, and for a team that already runs Kubernetes well, a genuinely good one. The server becomes an ordinary Deployment with an ordinary Service and an ordinary Ingress. The worker becomes another small Deployment. A Kubernetes work pool turns every flow run into a Job with its own resource requests, its own service account, its own image, and the cluster autoscaler underneath doing what it is good at. Secrets come from wherever your cluster's secrets already come from. It composes beautifully with everything else you already operate.
Three things to get right. Do not run the metadata Postgres in-cluster on ephemeral storage to save a line item — that database is the component whose loss is genuinely unrecoverable, and you do not want to be inventing its backup strategy under pressure. Be honest about per-run startup: a Job that must schedule a pod and pull a two-gigabyte image before executing forty seconds of Python is spending most of its life on logistics, and the fix is usually a warm node pool or fewer, chunkier flow runs rather than more YAML. And watch what happens to your idle bill, because the whole point of per-run Jobs is that the cluster scales down between them — which it will not do if a node is pinned by something else you forgot about. The standing rule applies here as everywhere: this is right if Kubernetes is already load-bearing for you, and wrong if you would be adopting Kubernetes in order to run Prefect.
3. One VM with systemd
The under-rated option, and the one I would push most small teams toward for the self-hosted case. A server unit, a worker unit, a managed Postgres beside it, and you are done. One machine, one journal to read, one thing to restart. A process pool if everything is trusted and shares a dependency set; a Docker pool on the same box if it does not, which gets you per-deployment images without a scheduler in the middle.
This handles more load than people expect. A single reasonably-sized VM will comfortably serve the orchestration needs of a company that has convinced itself it needs a cluster, and the operational legibility is worth a lot: when something is wrong, there is exactly one place to look. The ceilings are exactly where you would expect. You are bounded by one machine's RAM, so a run that wants 30GB is a resize rather than a scheduling decision. Bursts queue instead of spreading. And your worker's availability is your VM's availability, which is fine when your pipelines are idempotent and your run queue absorbs a gap, and merely awkward when a two-hour outage means a missed SLA.
# /etc/systemd/system/prefect-server.service
# API + UI + background services. Restartable; a running flow reconnects.
[Unit]
Description=Prefect server
After=network-online.target
[Service]
User=prefect
WorkingDirectory=/opt/prefect
# PREFECT_API_DATABASE_CONNECTION_URL lives here, 0600, root-owned, not in git.
EnvironmentFile=/etc/prefect/server.env
ExecStartPre=/opt/prefect/venv/bin/prefect server database upgrade -y
ExecStart=/opt/prefect/venv/bin/prefect server start --host 0.0.0.0 --port 4200
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# ---------------------------------------------------------------------------
# /etc/systemd/system/prefect-worker@.service
# Templated on the pool name, so `systemctl enable --now prefect-worker@etl`
# gives you a worker for the `etl` pool. Run as many as you have pools.
[Unit]
Description=Prefect worker for the %i work pool
After=network-online.target
[Service]
User=prefect
WorkingDirectory=/opt/prefect
# PREFECT_API_URL and PREFECT_API_KEY. Note that this file is readable by the
# prefect user -- and on a process pool, so is every flow run. If any flow
# executes code you did not write, that is your whole threat model in one line.
EnvironmentFile=/etc/prefect/worker.env
ExecStart=/opt/prefect/venv/bin/prefect worker start --pool %i
Restart=always
RestartSec=5
# Let in-flight runs report their final state before the process dies. A
# SIGKILLed worker leaves runs stuck in Running until something reconciles them.
KillSignal=SIGINT
TimeoutStopSec=120
[Install]
WantedBy=multi-user.targetTwo details in there are worth more than they look. The templated worker unit means adding a pool is one systemctl command rather than a new file, which matters once you have separate pools for cheap flows and expensive ones. And the SIGINT-plus-generous-timeout combination is the difference between a clean deploy and a UI full of runs stuck in Running because the worker was killed mid-report.
4. Container PaaS (Render, Railway, Fly, Northflank and that class)
This is a very natural home for the server half, and I mean that as praise. The Prefect server is an HTTP service with a Postgres attached — which is the exact thing this category of platform was designed to make trivial. You get TLS, a hostname, a managed Postgres one click away, deploy-on-push, and rollbacks. For question one, on a small-to-medium self-hosted setup, it is hard to beat for effort-to-outcome.
The worker half is where you need to read the fine print, and the specific thing to check is whether the platform will run a process that serves no HTTP traffic and keep it running. A Prefect worker has no port to health-check; it polls outbound and waits. Platforms that scale services down based on inbound request volume will happily scale your worker to zero, and a scaled-to-zero worker does not poll, which means your flow runs sit in Scheduled and then quietly turn Late while every dashboard stays green. Most platforms in this category do have a background-worker or private-service construct that solves this. Confirm its semantics — specifically whether it is guaranteed always-on and what its restart behaviour is — before you commit rather than after your first silently missed nightly.
The other honest limitation is per-run resource shape. On this class of platform your worker is typically a fixed-size instance running flow runs as subprocesses, which means every flow run gets the same shape, and your one memory-hungry backfill sizes the instance for all the others. That is a perfectly acceptable trade for a lot of teams. It is a bad one if your workload's long tail is genuinely long.
5. Serverless and functions platforms
The appeal is obvious and the fit is genuinely good for exactly one half of the system. The server cannot go here — it is a long-lived stateful-ish service with background loops, and a function platform is the wrong shape for it in every dimension. But the execution half maps well: a flow run is a bounded unit of work, it needs only outbound HTTPS, and it wants to not exist between invocations. That is a function, or near enough.
Two ways in. The clean one is a push work pool, if you are on Prefect Cloud: the run is submitted directly into your cloud account's serverless container service with no worker in the loop, which is the closest thing to a native answer this whole post contains. The scrappier one is to run a lightweight worker somewhere cheap and have the flows themselves fan the actual work out to functions, which works but means you now maintain two orchestration models and debug across the seam.
The constraints are the usual ones and they bite specifically here. Maximum execution duration is the big one — orchestrated pipelines have a long tail by nature, and a platform with a hard wall-clock ceiling will amputate exactly the nightly backfill you most needed to complete. Then cold starts on a large dependency set, memory ceilings, and whether the runtime can reach a VPC-private warehouse. Check every one of those against current documentation for the specific platform, because they vary widely and change often. Fit is excellent for short, uniform, well-bounded flows and poor for a mixed portfolio with a heavy tail.
6. A microVM per flow run (where we fit)
Read this as an interested party's description, because it is one. PandaStack is an open-source Firecracker microVM platform: managed PostgreSQL 16 with branch, clone and point-in-time restore, git-driven app hosting that scales to zero, serverless functions with cron, and sandboxes created by restoring a baked snapshot rather than booting.
The unglamorous half first. The server deploys here the way it deploys on any container PaaS — an app from git, with a managed Postgres beside it, TLS and a hostname included. That is a fine place for it and it is also not a differentiated claim; item four on this list does the same job well. The one caveat is the same one that applies there: the server may sleep when nobody is looking at the UI, but if you are relying on its background services to fire your schedules then it must not. Put it on an always-on app, not on scale-to-zero. I would rather write that sentence here than have you find it out on a Sunday.
The half that is actually different is the flow run. Because a sandbox is created by restoring a snapshot rather than booting a machine — around 179ms at p50 and 203ms at p99, with the roughly three-second cold boot paid once at bake time and amortised over every restore after it — creating a fresh machine per flow run stops being an optimisation you have to justify. That startup cost is small enough to disappear inside a run of any realistic length, which flips the burstiness argument from the previous section: you get the isolation properties of a per-run machine and the idle cost of nothing, without the warm pool that quietly erodes freshness on instance-backed fleets. Memory is copy-on-write and the rootfs is a reflink clone, so the hundredth guest does not copy gigabytes into existence, and each agent has 16,384 pre-allocated network slots so per-run networking is not the bottleneck either.
And it is a hardware boundary. Each guest is a Firecracker microVM with its own kernel under KVM, its own memory, and its own Linux network namespace with its own TAP device — so an egress allowlist covering your API, your git host and your package registry is a property of the machine rather than a rule you hope nothing bypasses. A flow run that pip-installs an arbitrary package, executes a customer's transform, or runs model-generated Python cannot reach the host, cannot reach the other runs, and cannot read anyone else's credentials. Because the VM is destroyed when the run finishes, cleanup is a consequence of the architecture rather than a script you have to get right.
Now the honest limits, and they are real. There is no first-party PandaStack work pool type in Prefect. What you do instead is run a small supervisor that asks the Prefect API which runs in a pool are ready, creates a sandbox for each, and executes exactly that one flow run inside it. That is maybe eighty lines of Python you own and maintain — a shape, not a plugin. It composes with anything: keep the control plane on Prefect Cloud, keep your ordinary flows on your ordinary worker, and route only the pool that runs untrusted code to disposable machines. But if what you want is a managed Prefect control plane, we are not that, and Prefect Cloud is the honest recommendation.
import asyncio
import os
from prefect.client.orchestration import get_client
from pandastack import Sandbox
POOL = "microvm-pool" # the pool your risky deployments use
API_URL = os.environ["PREFECT_API_URL"]
API_KEY = os.environ["PREFECT_API_KEY"]
MAX_IN_FLIGHT = 25
async def ready_runs(limit: int):
"""Ask Prefect which runs in this pool are ready to execute. Client method
names move between 3.x minors -- check yours; the shape is stable even
when the spelling is not."""
async with get_client() as client:
return await client.get_scheduled_flow_runs_for_work_pool(
work_pool_name=POOL, limit=limit,
)
def execute_in_its_own_machine(flow_run_id: str) -> None:
# 1. Restore a guest from a baked snapshot. Python, the pinned Prefect
# version and the repo's dependencies are already inside it, so nothing
# is installed on the hot path. ~179ms p50 -- this is a snapshot restore,
# not a boot. The ~3s cold boot was paid once, when the template baked.
sbx = Sandbox.create(
template="prefect-worker",
ttl_seconds=3600, # backstop: the guest reaps itself if we crash
metadata={"flow_run_id": flow_run_id, "pool": POOL},
)
try:
# 2. The API key arrives as a 0400 file, never as argv. argv is
# readable by every process in the guest, including whatever the
# flow decides to subprocess.
sbx.filesystem.write("/run/prefect/api-key", API_KEY)
sbx.exec("chmod 0400 /run/prefect/api-key")
# 3. Execute exactly this run, in this machine, and nothing else.
# Prefect reports state and logs outbound over HTTPS, so the guest
# needs no inbound port at all -- which is what lets the network
# namespace run default-deny inbound with an egress allowlist.
res = sbx.exec(
"set -a; PREFECT_API_URL='%s'; "
"PREFECT_API_KEY=$(cat /run/prefect/api-key); set +a; "
"rm -f /run/prefect/api-key; "
"prefect flow-run execute %s" % (API_URL, flow_run_id),
timeout_seconds=3300,
)
print(res.stdout[-2000:]) # tail, for our own observability
finally:
# 4. Delete the machine. The cloned repo, the pip cache, whatever the
# flow wrote to /tmp, whatever it forked and detached and hoped we
# would not notice -- all of it stops existing at the same instant.
# There is no cleanup script to get wrong because there is no
# cleanup.
sbx.kill()
def supervise() -> None:
in_flight = 0
while True:
runs = asyncio.run(ready_runs(MAX_IN_FLIGHT - in_flight))
for r in runs:
# Dispatch these onto a thread or task pool; one at a time here
# only so the example stays readable.
execute_in_its_own_machine(str(r.flow_run.id))
# Nothing scheduled means nothing running and nothing billed. That is
# the whole argument for the two-minutes-an-hour pipeline.
asyncio.run(asyncio.sleep(5))Side by side
Same caveat as everywhere: everything about other people's products here is qualitative and changes on their schedule, and only the PandaStack numbers are ones I measured.
- Prefect Cloud — Server hosting: fully managed, including the database, upgrades and SSO. Worker model: your own workers anywhere, or managed and push pools where Prefect or your cloud supplies the compute. Idle cost: none for the control plane; depends entirely on whether your workers are always-on. Isolation: whatever your chosen pool type gives you. Ops burden: lowest available. Best for: most teams, and nearly every team without a dedicated platform engineer.
- Kubernetes — Server hosting: a Deployment, a Service, an Ingress, plus a managed Postgres you should not put in-cluster. Worker model: one Job per flow run with per-deployment resource requests. Idle cost: whatever your cluster floor is, which is rarely zero. Isolation: namespaces and cgroups over a shared node kernel. Ops burden: inherits your cluster's; high if the cluster is new. Best for: teams already running Kubernetes with working GitOps.
- One VM with systemd — Server hosting: a unit file and a managed Postgres. Worker model: a process pool for trusted flows, a Docker pool on the same box when they need different dependency sets. Idle cost: one always-on machine, which is small and honest. Isolation: none between process-pool runs; container-level with a Docker pool. Ops burden: low and legible — one journal, one thing to restart. Best for: the large majority of self-hosters who think they need a cluster and do not.
- Container PaaS (Render / Railway / Fly / Northflank class) — Server hosting: excellent; this is the exact shape they were built for, with managed Postgres alongside. Worker model: an always-on background-worker service running flow runs as subprocesses. Idle cost: the worker instance, all night, every night. Isolation: shared kernel, shared instance, one resource shape for every run. Ops burden: low. Watch: confirm the platform guarantees an always-on non-HTTP process, or your worker sleeps and runs go Late in silence. Best for: small self-hosted setups that want a hostname and a deploy pipeline for free.
- Serverless / functions platforms — Server hosting: not viable; wrong shape for a long-lived service with background loops. Worker model: push pools on Prefect Cloud, or flows that fan work out to functions. Idle cost: genuinely zero. Isolation: the provider's, typically a shared-kernel sandbox — check what they actually promise. Ops burden: low, until a duration ceiling truncates a backfill. Best for: short, uniform, well-bounded flows with no long tail.
- MicroVM per flow run (PandaStack) — Server hosting: an always-on app from git with a managed Postgres beside it — competent, not differentiated. Worker model: a supervisor creates a fresh Firecracker guest per flow run and deletes it after, at ~179ms p50 / 203ms p99 to create. Idle cost: zero, without a warm pool eroding freshness. Isolation: hardware-level — its own guest kernel under KVM, its own memory, its own network namespace. Ops burden: an integration you own, roughly a page of Python, plus normal app hosting for the server. Best for: platforms whose flows execute customer-supplied or model-generated code.
The five things that actually bite in production
Independent of which option you pick. I have watched every one of these cost someone a day.
- Nobody is polling the pool. This is the number one self-hosted Prefect support question and it presents as a mystery: the schedule is correct, the deployment exists, the UI is green, and the run just sits there and then goes Late. Almost always the worker is not running, is pointed at a different API URL, or is polling a different pool than the deployment targets. Alert on the age of the oldest Scheduled run in each pool. That single alert catches more incidents than any dashboard.
- Results live on the machine that ran the flow. Prefect persists results to a local path by default, and on any platform where the run's machine is disposable, that path disappears with it. This matters more than it sounds: retries, caching and any downstream flow reading an upstream result all depend on results surviving. Configure object-storage result storage before your first ephemeral-infrastructure deployment, not after the retry that could not find its own input.
- Logs versus stdout. Prefect ships logger output to the API, which is genuinely lovely — your logs outlive the machine, and you can read them in the UI. But raw stdout and stderr from a subprocess your flow shelled out to is not automatically in that stream. The classic version of this is a flow that calls a CLI tool, the tool fails, and the UI shows a non-zero exit code with no explanation because the actual error went to a file descriptor nobody captured.
- Concurrency you did not set. A work pool with no concurrency limit will happily launch every run a backfill produces at the same instant. The first time someone backfills a year of daily partitions, three hundred and sixty-five flow runs try to open connections to the same warehouse and the same metadata database at once. Set pool-level limits, and use tag-based limits for the shared resource that will actually be the bottleneck.
- Timezones, and the hour everyone chose. A cron schedule with no explicit timezone will find a DST boundary eventually, and it will find it during a quarter close. Separately: everything in your company runs on the hour, so your database, your warehouse and your upstream APIs all get hit simultaneously by systems that had no idea about each other. Stagger deliberately. Seven minutes past is free.
How to choose, in ten minutes
- Answer the two questions separately and write them down as two lines. Where does the server live; where do flow runs execute. If you find yourself giving one answer to both, you have not actually decided yet.
- Ask whether your metadata may leave your infrastructure. If yes, start at Prefect Cloud for question one and only leave it for a concrete blocker. If no, you are self-hosting a web service and a Postgres, which is an evening's work and a small permanent obligation.
- Plot your flow runs for a week: count, duration, and the shape of the tail. If the median run is short and the fleet is idle most of the time, per-run infrastructure will beat an always-on worker — provided the per-run startup cost is small relative to the run. If your runs are long and uniform, an always-on worker is cheap and simple and you should stop reading comparison posts.
- Write down the largest single flow run you will ever need: RAM, wall clock, GPU or not, VPC-private endpoint or not. That one number eliminates more options than any feature grid, and it is the number that kills serverless for mixed portfolios.
- Ask the isolation question explicitly. Will any flow run ever execute code that a customer, a partner or a model wrote? If yes, decide now whether a shared kernel is acceptable, because that decision reaches into credentials, networking and concurrency and is expensive to reverse later.
- Then prove it with a spike rather than a spreadsheet. Deploy the candidate, point it at a deliberately undersized database, and run a backfill over a year of daily partitions with no concurrency limit set. You will learn more in an afternoon than in a week of vendor pages, and you will learn it while nobody is depending on the answer.
The short version
Prefect Cloud for the server if your metadata can leave your perimeter, which for most teams it can, and there is no shame in buying the boring half. One VM with systemd and a managed Postgres if it cannot and you are smaller than you think you are, which is more common than the conference talks suggest. Kubernetes if the cluster already exists and someone already knows it, never because Prefect made you buy one. Container PaaS for the server plus an always-on background worker if you want a hostname and a deploy pipeline for free — just confirm that worker is genuinely always-on. Serverless for the execution half only, and only if your runs are short and uniform. And a microVM per flow run, from us or from anyone, for the specific case where the code inside the run is not code you wrote.
Whichever you choose, the decisions that will still matter in a year are the same on every platform. Postgres rather than SQLite the moment anything is concurrent. An alert on the age of the oldest Scheduled run, because a pool nobody is polling looks exactly like a quiet afternoon. Results and logs in storage that outlives the machine that produced them. Concurrency limits set before the first backfill rather than after it. And an honest answer, written down, to the question of whose code executes inside your flow runs. Get those right and hosting stops being the interesting question — which, for an orchestrator, is precisely the goal.
Frequently asked questions
Do I need Postgres to self-host Prefect, or is SQLite enough?
SQLite is Prefect's default so that a fresh install works with no dependencies, and it is genuinely fine for a laptop and defensible for a single-user server ticking over a handful of flows an hour. It stops being fine as soon as anything is concurrent, and Prefect's write pattern is unusually concurrent for a metadata store: every task-run state transition, every log line and every worker heartbeat lands at once whenever your schedules cluster — which they do, because everyone writes cron expressions ending in zero. The failure mode is not a crash but database-is-locked errors, a UI that spins and state updates that time out while the flow itself is running perfectly. Use Postgres, and set the connection URL before the very first server start, because a server that has already created its SQLite file will keep using it quite happily while you wonder why the new database is empty.
What is the difference between a work pool and a worker in Prefect?
They are two halves of one mechanism and the distinction matters when you are choosing a platform. A work pool is a named queue plus a description of the infrastructure a flow run should get — its type decides whether that means a subprocess, a Docker container, a Kubernetes Job or a cloud container task, and its base job template holds the defaults that individual deployments override through job variables. A worker is the lightweight long-lived process that polls one pool, finds scheduled runs and provisions that infrastructure for each of them. Workers only ever dial outbound to the API, so they need no inbound port and work identically behind three firewalls. The exceptions are push work pools, where there is no worker at all because Prefect Cloud submits runs directly into your cloud account's serverless container service, and managed pools where Prefect supplies the compute — both of which are Cloud features rather than open-source server ones.
Why do my Prefect flow runs stay Scheduled and then go Late?
In almost every case it means nothing is polling the pool that the deployment targets. The three usual causes, in order of frequency: the worker process is not actually running; the worker is pointed at a different PREFECT_API_URL than the one you deployed against, which happens constantly when one of them omits the trailing /api; or the worker is polling a different pool or work queue than the deployment specifies, which is easy to do after a rename. A fourth cause is specific to hosting platforms that scale services down when no HTTP traffic arrives — a Prefect worker serves no HTTP, so a request-driven autoscaler will scale it to zero and your schedules will silently stop firing while every dashboard stays green. The single most valuable alert you can build is on the age of the oldest Scheduled flow run in each pool; it catches all four of these before a human notices.
Should I run Prefect Cloud or self-host the Prefect server?
Self-hosting the server is not hard — it is a web service and a Postgres, and it restarts cleanly without disturbing running flows — but it is a permanent small obligation: a database to back up, TLS to renew, an authentication story to invent because the open-source server has historically shipped without one, an upgrade cadence to keep up with, and alerting that exists only because you self-hosted. None of that is differentiated work. Unless you have a compliance requirement that keeps orchestration metadata inside your perimeter, or a cost model that genuinely favours self-hosting at your scale, Prefect Cloud is the choice that lets you spend attention on pipelines instead. It is also where push and managed work pools live, so if your motivation for self-hosting was avoiding an always-on worker, check whether the hosted product already solves that. Verify current tiers, limits and RBAC against Prefect's documentation rather than any blog post, including this one.
How do I isolate untrusted or customer-supplied code in a Prefect flow run?
Start by separating two problems that look alike. Resource isolation stops one flow run starving another, and container-based work pools do that well. Security isolation is a different question, and it becomes urgent the moment a flow pip-installs a customer's requirements file, executes a transform an analyst wrote and a model finished, or runs code a language model emitted. That code executes with the flow run's credentials — the warehouse DSN, the object-storage token, the Prefect API key, the cloud metadata endpoint one HTTP request away — and on a process work pool it also shares a filesystem and a kernel with every other tenant's runs. A container improves this substantially but the boundary is still namespaces and cgroups over a shared host kernel. When the code is not yours, the boundary you want is a hypervisor: a microVM per flow run, with its own guest kernel under KVM, its own memory and its own network namespace, created for that run and destroyed when it finishes so cleanup is architectural rather than a script you have to get right.
Keep reading
- Best Dagster hosting platforms in 2026 — The closest sibling — where a daemon that must be a singleton replaces the work-pool question entirely.
- Best Apache Airflow hosting platforms in 2026 — The same two-question split, except Airflow's shared worker environment makes dependency hell the dominant constraint.
- Best Temporal hosting platforms in 2026 — Read this if your flows are really long-running business processes wearing a pipeline costume.
- Per-tenant isolation for workflow engines — The deeper version of the untrusted-flow-code problem, with the worker-side pattern spelled out.
- Always-on vs scale-to-zero infrastructure — The idle-cost arithmetic behind the two-minutes-an-hour pipeline, in general form.
- Firecracker sandboxes on PandaStack — What a microVM per flow run actually is — snapshot-restore creates, per-sandbox network namespaces, TTL reaping.
49ms p50 cold start. Fork, snapshot, and scale to zero.