Best Platforms to Host Python APIs for Free
Free hosting is the right call for exactly one situation: you have paying-customer risk but no paying customers yet, and every dollar you do not spend on infrastructure is a dollar of runway. It stops being the right call the moment a customer's request times out. This page resolves that choice with numbers instead of marketing copy — the real ceilings on Render, Vercel, Fly.io and Railway, what a cold start actually costs in seconds, the deploy configs that survive those ceilings, and the specific signals that mean it is time to pay. Part of the Integrating Stripe with Python APIs guide, so you can wire billing in the moment the host is settled.
What "free" actually buys you
Every free tier is the same bet from the platform's side: give away enough compute to get you deploying, then charge when the app matters. That bet shapes four constraints you cannot engineer around, only design for.
The filesystem is a lie. Anything you write to local disk — SQLite files, uploaded images, log files, a Whisper model you downloaded at boot — disappears on the next deploy, restart, or machine move. Treat the container as read-only and push every byte of state to managed Postgres, Redis, or object storage.
Idle instances sleep. Render's free web services spin down after roughly 15 minutes with no traffic. Fly Machines auto-stop when you configure them to. Vercel functions never stay warm at all. Sleep is not a bug; it is how the free tier pays for itself.
RAM is the binding limit, not CPU. A FastAPI app importing pandas, a Stripe client, and SQLAlchemy sits near 180–220 MB resident before it serves a single request. On a 256 MB Fly Machine that leaves almost nothing for concurrency, and the OOM killer returns a bare 503 with no traceback in your logs. Shrinking imports matters more than shrinking code, and the same discipline behind optimizing Python Docker image size applies directly.
Bandwidth and build minutes cap hard. Serving uncompressed 400 KB JSON responses to 10,000 requests burns 4 GB. Free bandwidth allowances sit in the low tens of gigabytes; past them platforms throttle, 429, or suspend without a courtesy email.
The four platforms, ranked for Python
Render is the default. A free web service runs a real long-lived ASGI process, gives you automatic HTTPS on a subdomain, and deploys from a GitHub push with no CLI. It is the shortest path from uvicorn app.main:app on your laptop to a public URL. Its one disqualifying flaw is the spin-down: a customer hitting a sleeping instance waits the better part of a minute.
Fly.io is the technically strongest free-adjacent option. Machines start from a stopped state in well under a second, so auto_stop_machines gives you most of Render's cost profile without Render's wake penalty. The price is that you manage a Dockerfile, health checks, and regions yourself, and the free allowance is small enough that a 256 MB Machine is your realistic budget.
Railway is a trial, not a free tier. You get credits, they run out, and the app stops. What you buy with those credits is the fastest possible path to a Python API plus managed Postgres in one project, which makes it excellent for a two-week validation build and wrong for anything you leave running.
Vercel is for stateless edges only. Python functions there are per-request processes with a hard execution timeout in the ten-second range, no WebSockets, and no background work after the response is sent. Ship a webhook receiver or a small JSON transform there; do not ship a subscription API. If edge-shaped deployment genuinely fits your workload, deploying FastAPI to Cloudflare Workers with Python is a stronger version of the same idea.
The commercial framing matters more than the feature list. The cheapest always-on plan on each platform is the number you are really comparing against, because that is what you pay the day free stops working.
Three of the four sit inside a rounding error of each other. That is the whole argument: the gap between free and always-on is roughly one paid customer, and a longer breakdown of how those three behave under load lives in Render vs Railway vs Fly.io.
The cold-start tax, measured
A spun-down Render instance does not "wake slowly" — it performs a full container start. The platform provisions a machine, pulls your image, runs the Python interpreter, imports every module in your dependency graph, executes your lifespan startup, and only then binds the port. Import time dominates: a lean FastAPI app imports in 300–600 ms, but add pandas, boto3 and a heavy ORM and you are at three to five seconds of import alone before any of the platform overhead.
The cruelty of the distribution is that the tax lands on the wrong person. Your busy hours stay warm; the prospect who clicks your demo link at 3 a.m. gets the 42-second version and never comes back. Averaged over a day the p50 looks fine and the p95 is a business problem, which is exactly the distinction drawn in calculating cost per API request.
A keep-alive ping is the honest mitigation, provided you run it externally rather than as an in-process loop, which most terms of service forbid. Ten-minute intervals from GitHub Actions cost nothing and keep a Render instance from ever sleeping.
name: keep-alive
on:
schedule:
- cron: "*/10 * * * *"
jobs:
ping:
runs-on: ubuntu-latest
steps:
- name: Health check
env:
API_HEALTH_URL: ${{ secrets.API_HEALTH_URL }}
run: |
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "$API_HEALTH_URL")
test "$code" = "200" || { echo "unhealthy: $code"; exit 1; }
The deploy config that survives a free tier
Two settings cause most free-tier 502s: too many workers for the RAM you have, and a health check pointed at a route that touches the database. Read both from the environment so the same image runs locally and in production.
# app/main.py
import os
from contextlib import asynccontextmanager
import httpx
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# One shared client. Never open a connection pool per request.
app.state.http = httpx.AsyncClient(
timeout=httpx.Timeout(float(os.getenv("HTTP_TIMEOUT_SECONDS", "8"))),
limits=httpx.Limits(max_connections=int(os.getenv("HTTP_MAX_CONNS", "10"))),
)
yield
await app.state.http.aclose()
app = FastAPI(lifespan=lifespan, docs_url=os.getenv("DOCS_URL", "/docs"))
@app.get("/healthz")
async def healthz() -> dict[str, str]:
# Cheap and dependency-free: the platform probe must never hit Postgres.
return {"status": "ok", "release": os.getenv("RELEASE_SHA", "dev")}
Worker count follows memory, not CPU count. On a 512 MB instance two Uvicorn workers is the ceiling and one is often correct; the reasoning is unpacked in Uvicorn vs Gunicorn worker configuration.
# scripts/serve.py — resolve the start command from the platform's own env vars.
import os
match os.getenv("PLATFORM", "local"):
case "render" | "railway":
workers = int(os.getenv("WEB_CONCURRENCY", "1"))
case "fly":
workers = 1 # 256 MB Machines fit exactly one worker
case _:
workers = 1
port = int(os.getenv("PORT", "8000"))
os.execvp("uvicorn", [
"uvicorn", "app.main:app",
"--host", "0.0.0.0", "--port", str(port),
"--workers", str(workers),
])
Everything stateful goes outside the container. Free Postgres tiers cap connections aggressively — often 20 to 25 — so pin the pool small (pool_size=3, max_overflow=0) and expect trouble if you skip it; the symptoms and fixes are in fixing connection pool exhaustion. Cache aggressively too, since a cached response is a request you never pay compute for, and on a single small instance the trade-offs in Redis vs in-memory caching for FastAPI tilt differently than they would on a fleet.
Wiring in payments before the traffic arrives
Free hosting is a launchpad, not a business model, and the sequencing mistake builders make is waiting until traffic justifies billing. Add the money path while the app is small enough to change. Three pieces matter on a constrained host.
Stripe needs a publicly reachable webhook route, and on a sleeping instance Stripe's delivery attempt is the request that triggers the 42-second cold start. Stripe retries, so you rarely lose the event, but you must verify signatures on the raw body before parsing — the exact procedure is in verifying Stripe webhook signatures — and your handler must be idempotent, because a retried event will arrive twice.
Quota enforcement belongs in middleware backed by Redis, not in a per-route decorator reading Postgres. Counting in Redis keeps the check under a millisecond and keeps your one database connection free for real work; the enforcement patterns sit alongside preventing free-tier abuse and the client-side etiquette in best practices for API rate limiting.
Degrade instead of dying. When you approach a platform ceiling, return 429 with a Retry-After header and serve cached data where you can. A customer who sees a documented back-off treats you as a real service; one who sees a 502 opens a competitor's tab. Feeding those events into tracking API usage and analytics also gives you the evidence for when to upgrade.
When to graduate
Migrate when the friction costs more than the instance. Four signals settle it, and any single one is enough.
Run the arithmetic honestly. If you spend ninety minutes a month babysitting a keep-alive workflow, chasing OOM kills, and apologising for slow first requests, and you value your time at even $30 an hour, the free tier costs you $45 a month to avoid a $7 bill. That is the real comparison, and it is why every serious answer to "what should I host on for free?" ends with "for about six weeks."
Common mistakes that end free-tier deploys
- Writing to local disk. SQLite, uploads, and log files vanish on restart, and you find out during a customer demo.
- Loading models at import time. A 400 MB embedding model turns a five-second cold start into an OOM kill on a 512 MB instance.
- Health checks that query the database. The probe fails when Postgres hiccups, the platform restarts a healthy app, and you get a restart loop.
- Hardcoded secrets. Committed keys defeat platform secret injection and force an emergency rotation you have not practised.
- Uncompressed payloads. Enable gzip and pagination or bandwidth caps arrive far sooner than you projected.
- Deploying with no rollback. Free tiers restart in place; the safety patterns in zero-downtime deploys for Python APIs still apply.
Builder verdict
Start on Render because it gets a real ASGI process on a public HTTPS URL faster than anything else, and pair it with an external keep-alive ping so nobody meets the spin-down. Reach for Fly.io instead if you are comfortable with Docker and want sub-second wakes and regional placement from day one. Use Railway for a fixed-length validation sprint where managed Postgres in one click is worth burning credits. Send only genuinely stateless edge work to Vercel. Then set yourself a trigger — first paying customer, or first support message about slowness — and upgrade to the $5–$7 always-on plan without agonising. The free tier is a way to test whether anyone wants the product; once someone does, the cheapest paid instance on the market is not the expense worth optimising. Route that energy into Building & Monetizing API-Driven Micro-SaaS instead.
FAQ
How many paying customers does it take to cover leaving the free tier? One, at almost any price point. The cheapest always-on plans run $3–$7 a month, so a single $9 subscription covers hosting plus Stripe's fee with margin left over. Anyone still on free hosting after their first sale is optimising the wrong line item.
Can I run WebSockets or streaming responses on a free Python host? Not on Vercel — its Python functions are request-scoped with a hard timeout. Render's free web services and Fly Machines both run a persistent process and handle WebSockets and server-sent events fine, though a sleeping instance drops connections when it spins down. Streaming responses through a long-lived ASGI process is the pattern that works.
What actually happens when I blow through a free tier limit? It depends on which limit. Memory overruns get an immediate OOM kill and a 503. Bandwidth and build-minute overruns usually mean throttling or a suspended service until the cycle resets. Railway simply stops the app when credits hit zero. None of these send a warning first, so alert on your own metrics rather than trusting the dashboard.
Is migrating off a free tier risky once customers depend on the API? Only if you built for one platform. Keep every setting in environment variables, keep state in managed Postgres and object storage, and a migration becomes a new deploy plus a DNS change with a low TTL. Point the new host at the same database, verify with the health endpoint, cut over, and keep the old instance running for a day as a fallback.
Do free tiers break Stripe webhooks? They delay them rather than break them. A sleeping instance can take 30–60 seconds to answer Stripe's delivery, which may register as a failure; Stripe then retries with exponential back-off, so the event still lands if your handler is idempotent. A keep-alive ping removes the problem entirely and costs nothing.
Related
Same track:
- Integrating Stripe with Python APIs — wire billing in as soon as the host is chosen.
- Render vs Railway vs Fly.io — the paid-tier head-to-head once free stops working.
- Deploying APIs to Render or Vercel — the full deployment walkthrough for both models.
- Calculating cost per API request — turn instance price into a per-request margin figure.
Other tracks:
- Fixing connection pool exhaustion — the failure you will hit first on a free Postgres plan.
- Optimizing Python Docker image size — smaller images mean shorter cold starts.