all posts

The best Python hosting platforms in 2026

Ajay Kumar··9 min read

Python deploys fail in a small number of very specific ways, and none of them appear on a pricing page. Wheels that don't exist for the platform's architecture. A worker count guessed from a template rather than from the app. An ASGI app started with a WSGI server so every request blocks. Celery workers that were supposed to be a second process and quietly became a thread inside the web process.

So this isn't a ranking. It's the set of questions that determine which platform will be boring for your app, and I'll name the categories that answer each one. I build PandaStack, which is in the last category — I'll flag the bias.

Question one: which server, and how many workers?

Django and Flask are WSGI: one request per worker process at a time. FastAPI, Starlette, and modern Django with async views are ASGI: one process handles many concurrent requests, provided the code actually awaits rather than blocking.

The mistake that costs people a week is running an ASGI app under a plain WSGI server, or running an async app with sync database calls. Both produce the same symptom — throughput that doesn't improve when you add workers — and both are invisible until load arrives.

# WSGI (Django, Flask): worker count is your concurrency
gunicorn myapp.wsgi:application --workers 3 --bind 0.0.0.0:$PORT

# ASGI (FastAPI, Starlette): one process handles many requests
uvicorn app.main:app --host 0.0.0.0 --port $PORT

# Django with async views, run under an ASGI worker class
gunicorn myapp.asgi:application -k uvicorn.workers.UvicornWorker \
  --workers 2 --bind 0.0.0.0:$PORT
Worker count is a memory calculation, not a preference. Each Gunicorn worker is a full copy of your interpreter and imports — commonly 150–400 MB for a Django app with the usual dependency set. Four workers on a 512 MB instance will be killed by the OOM reaper under load, and the log will say nothing more useful than 'worker exited'.

The categories, and what each is good at

1. Serverless functions — Lambda, Cloud Functions, Vercel

Good for request-scoped, spiky, stateless work. Genuinely cheap when traffic is intermittent. Two structural mismatches with a typical Python web app: cold starts get worse as your dependency tree grows, because importing pandas or a large ORM costs real time on every cold invocation; and there is no long-running process, so Celery workers, scheduled jobs, and WebSockets all become separate infrastructure.

Choose it for glue and event handlers. Don't choose it to avoid running a server if your app genuinely needs one.

2. Python-native compute platforms — Modal, Beam, Replicate

Built for Python specifically, usually around ML and batch workloads, often with GPU access and a decorator-based programming model. If your workload is 'run this function on a GPU' or 'fan this out across 200 containers', these are excellent and the ergonomics are miles ahead of generic infrastructure.

They are less of a fit for a conventional web app with a database and sessions, because you adopt their programming model rather than deploying your existing one.

3. Container PaaS — Render, Railway, Fly.io, Heroku, Northflank

The default answer for a Django or FastAPI app. Push a repo, they detect Python, install from requirements.txt or a lockfile, run your start command. Background workers and cron are first-class. Managed Postgres is one click away.

What to check: whether build detection handles your package manager (Poetry, uv, PDM, and plain pip all behave differently), whether there is a pre-traffic release hook to run migrations, and how much memory the build container has when a package needs compiling from source.

4. MicroVM platforms — Fly Machines, PandaStack

Same deploy shape as a PaaS, with a hardware-isolated VM and its own kernel instead of a shared-kernel container. My category. It's the right pick when you run code you didn't write — a notebook backend, a customer-supplied script, an LLM agent executing generated Python — or when per-tenant isolation is something you have to defend rather than describe. It's also relevant when your Python app creates environments at runtime: on a snapshot-restore substrate a fresh machine is around 179ms at p50, so 'one sandbox per user request' stops being an architectural decision and becomes a function call.

If you have one Django app and no untrusted code, a container PaaS is simpler and I'd say so.

The dependency problem nobody warns you about

Python's binary wheels are per-platform and per-architecture. A wheel that installs instantly on your Apple Silicon laptop may not exist for the platform's Linux target, at which point pip falls back to building from source — and that needs a C toolchain, headers, and often several minutes and a gigabyte of memory that the build container doesn't have.

  • Pin the Python version in the repo (.python-version, or the requires-python field), so the build resolves the same wheels your lockfile assumed.
  • Use a lockfile. `pip install -r requirements.txt` with unpinned transitive dependencies means the version resolved today isn't the one resolved next month.
  • Check whether your platform's build image includes a compiler before you depend on psycopg2 (source) rather than psycopg2-binary (wheel), or on anything with a Rust extension.
  • If the build is slow, look at what's compiling before you look at the platform. It's usually one package.
# Pin the interpreter in the repo so build and local agree
echo "3.12" > .python-version

# Deploy from git — detection reads .python-version and the lockfile
pandastack apps create --name billing-api \
  --git-url https://github.com/acme/billing \
  --start-cmd 'uvicorn app.main:app --host 0.0.0.0 --port $PORT'
Console scripts installed by pip — uvicorn, gunicorn, celery — land in the interpreter's own bin directory, not necessarily on the PATH your start command inherits. If a start command dies with 'uvicorn: not found' while the package is definitely installed, that's a PATH problem, not a dependency problem. Invoking through `python -m uvicorn` sidesteps it entirely.

Five checks before you commit

  1. Deploy your real app and time the build. If it takes eleven minutes because one package compiles from source, that's your feedback loop now.
  2. Load-test with the worker count you'll actually run, and watch memory. The OOM kill is silent and looks like a random restart.
  3. Start a background worker and a scheduled job. This is where serverless platforms stop matching a Python app.
  4. Run a database migration through the platform's release hook, and confirm a failing migration blocks the deploy instead of crash-looping the app.
  5. Check the architecture your build runs on. An arm64 build host and an amd64 runtime is a wheel mismatch waiting to happen.

The short version

A normal Django or FastAPI app with a database: a container PaaS, and spend the saved decision-making on getting the worker count and migrations right. Batch or GPU work: a Python-native compute platform. Event glue: serverless functions. Untrusted or user-generated Python, per-tenant isolation, or an app that provisions environments per request: microVM platforms, mine included. Whatever you pick, the platform is rarely what breaks — the worker count, the wheels, and the migration hook are.

Frequently asked questions

How many Gunicorn workers should I run?

Treat it as a memory calculation rather than a formula. Measure the resident memory of one worker under real load — a Django app with the usual dependency set is commonly 150–400 MB — then divide your instance memory by that, leave headroom of at least 25 percent, and cap the result. The frequently-quoted 2×CPU+1 rule assumes memory is free, which it isn't on a small instance, and the failure mode is ugly: the OOM killer takes a worker mid-request and the log says only that a worker exited. For ASGI apps the calculation changes shape entirely, because concurrency comes from the event loop rather than from process count, so a small number of workers is usually correct.

Do I need a Dockerfile to deploy a Python app?

Not on any platform with build detection. A repo with requirements.txt, pyproject.toml, or a lockfile is enough for the platform to install dependencies and start your app; you supply the start command, or it infers one. Pin the interpreter version in the repo with a .python-version file or the requires-python field so the build resolves the same wheels your lockfile assumed. A Dockerfile earns its place when you need system packages the build image lacks — a specific libpq, image codecs, a proprietary driver — or when you want the identical image running in CI and production.

Why is my Python deploy slow or failing during install?

Almost always one package building from source. Python wheels are per-platform and per-architecture, and when a matching wheel doesn't exist, pip compiles instead — which needs a C toolchain, headers, several minutes, and often more memory than the build container has. Read the install log for the package that starts compiling and address that one: use the binary variant where there is one, check whether your build architecture matches your runtime architecture, and confirm the build image ships a compiler if you genuinely need to build. Blaming the platform's CPU is the common wrong turn here.

Can I run Celery workers on a serverless platform?

Not as Celery. Serverless platforms have no long-running process to host a worker that polls a broker, so the standard pattern is replaced: the queue becomes a managed queue service, the worker becomes a function triggered by messages on it, and Celery's scheduler becomes the platform's cron. That works, and for spiky workloads it is cheaper, but it is a rewrite rather than a migration, and retries, visibility timeouts, and result storage all have to be re-established. If you want your existing Celery setup to keep working unchanged, deploy it as a second long-running process on a container or microVM platform.

Where should I host a Python app that runs untrusted or AI-generated code?

Somewhere the isolation boundary is a kernel boundary rather than a namespace. Running generated Python inside your web process gives an attacker your database credentials the moment they escape the interpreter, and Python has no meaningful in-process sandbox — restricted execution has been repeatedly broken and is not a security control. The workable options are a separate microVM or hardware-isolated sandbox per execution, with its own kernel, no ambient credentials, an egress policy, and a hard timeout. Fast microVM creation is what makes this practical rather than theoretical: when a fresh machine costs a couple of hundred milliseconds, one per execution is affordable.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.