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.

Free-tier ceilings across four Python hosts A matrix comparing Render, Vercel, Fly.io and Railway on idle behaviour, wake latency, RAM ceiling and local disk durability. The four ceilings that decide your host Free tier Render Vercel Fly.io Railway Idle behaviour sleeps 15 min per request auto-stop warm on credit Wake latency 30-60 s 1-4 s under 1 s none RAM ceiling 512 MB 1024 MB 256 MB 512 MB Local disk ephemeral read-only volume opt. ephemeral

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.

Cheapest always-on plan per platform Horizontal bar chart of approximate monthly cost to keep one small Python API permanently warm: Fly.io three dollars, Railway five, Render seven, Vercel twenty. Cost to keep one small API always warm (USD / month) Fly.io Railway Render Vercel Pro $3 $5 $7 $20 0 5 10 15 20 Approximate list prices at publication — confirm current rates before you commit.

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.

Timeline of one request to a sleeping instance A cold request spends six seconds booting the machine, twenty-two seconds pulling the image and importing dependencies, then fourteen more before the first response; a warm request answers in forty milliseconds. One request to a spun-down free instance spun down boot VM pull image + import deps lifespan, bind port, 200 OK 0-6 s 6-28 s 28-42 s 0s 10s 20s 30s 40s already warm 40 ms — identical code, identical host the tax lands on your rarest visitor

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.

Yaml
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.

Python
# 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.

Python
# 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.

Where state lives when the container is disposable A client hits the platform router, which fans out to either a per-request serverless function or a long-lived ASGI process; both read and write only external state: managed Postgres, Redis and object storage. Client TLS + router Serverless function fresh process per call Long-lived ASGI one warm event loop Managed Postgres pool_size 3, no overflow Redis cache + counters quota and rate limits Object storage uploads, exports, backups external state survives restarts container disk does not

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.

Decision tree for leaving the free tier Four yes-or-no checks — wake latency, pool exhaustion, region spread and time lost to workarounds — each routing to a specific paid upgrade, with all-no meaning stay free. p95 wake above 500 ms for paying users? Buy the always-on instance yes no Postgres pool exhausted at 10 users? Managed Postgres + PgBouncer yes no Customers outside your single region? Multi-region Fly or edge workers yes no Over an hour a month on workarounds? Pay the $7 and ship features yes All four no — stay free and keep shipping.

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.

Same track:

Other tracks: