Skip to content

Other hosts

This page is about Track 1 — hosting the agent. Render is the primary path and FastAPI Cloud is the second; both are verified end to end with this project. Neither is special — nothing in app/ knows which platform it’s on. This is the wider map, and more usefully, the one test to apply to any host you’re considering.

Looking for somewhere to put the frontend instead? That’s Track 2 — Deploy the client, and the answer is much shorter.

This project depends on work continuing after the HTTP response is sent. So:

Does my code still get CPU after it has returned a response?

Ask that first. Free tiers, cold starts, and dashboards are details; this one answers whether the architecture works at all. Backend hosts fall into two families.

Family Examples BackgroundTasks works?
Long-lived process — a container that stays running between requests Render, FastAPI Cloud, Fly.io, Koyeb, Railway, Hugging Face Spaces Yes. This is what the workshop assumes
Request-scoped serverless — CPU is allocated per invocation Vercel, Netlify, AWS Lambda No, not safely. Background work is capped by the function timeout

Everything below is a variation on those two rows.

Platform Family Free tier Good for
Render long-lived yes, no card Primary. Boring in the right way
FastAPI Cloud long-lived (scale-to-zero) yes, no card Second target. GitHub or one command; public beta
Hugging Face Spaces long-lived yes, no card A public, shareable demo with a URL people trust
Fly.io / Koyeb / Railway long-lived varies, often needs a card Regions, persistent disks, growing past free
Vercel request-scoped yes, no card The frontend. See the caveat
GitHub Pages static yes The built client/, and making CORS real

Free CPU hosting with a URL people already recognise. Use the Docker SDK — the Gradio and Static SDKs won’t run a FastAPI app.

Put this at the top of the Space’s README.md:

README.md
---
title: Research Agent
emoji: 🔎
colorFrom: blue
colorTo: purple
sdk: docker
app_port: 8000
---

app_port must match what your server binds. Spaces defaults to 7860, so either set it as above or start uvicorn on 7860.

Dockerfile
FROM python:3.12
RUN useradd -m -u 1000 user
WORKDIR /app
COPY --chown=user . /app
RUN pip install uv && uv sync --frozen
CMD ["uv", "run", "fastapi", "run", "main.py", "--host", "0.0.0.0", "--port", "8000"]

Same rule as everywhere: bind 0.0.0.0, never 127.0.0.1.

What it costs. Free CPU hardware (cpu-basic), and Spaces sleep after a configurable idle period — same cold-start trade as Render. The filesystem is ephemeral; Hugging Face has retired its persistent storage add-on, so treat disk as scratch space and keep state in Supabase. That’s already how this project works, so nothing changes.

This section is about running the backend on Vercel. Running the client there is a different question with a boring answer — it’s a static bundle, it works fine, and it’s the bonus track.

Vercel runs FastAPI, and it will happily serve /health, /docs, and /runs/naive. The problem is the endpoint the whole workshop is about.

You control the ceiling in vercel.json:

vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"functions": {
"app/main.py": { "maxDuration": 60 }
}
}

And there’s an API for work that outlives the response — waitUntil() — but read its documented behaviour carefully:

Promises passed to waitUntil are subject to the function’s overall timeout.

So the wall doesn’t disappear; it moves. A 40-second agent run under a 60-second cap works right up until the run that takes 70 seconds, and then it dies halfway with the run row stuck at running. That’s a worse failure than the 504 we started with, because it looks like it’s working.

This is the modern version of the old “Vercel has a 10-second limit” line. The number was raised and will move again. The shape of the constraint — CPU is rented per request — is the part that doesn’t expire.

Documentation about background execution is often vague. Measure instead:

  1. Deploy, then POST /runs with a real question.
  2. Close the tab. Wait a minute.
  3. GET /runs/{id}.

Steps kept appearing while nobody was polling? The host gives your process CPU between requests — the architecture works.

Steps stalled and only resumed when you polled? CPU is being allocated per request. Workable for a demo, but you now know exactly why it’ll break, and it won’t be a surprise at the worst moment.

Write down what you measured. A number you observed beats a paragraph you read.

Portability isn’t luck — it’s a consequence of the app knowing nothing about its host. The only file here that mentions Render is deploy/render.yaml. Pick a host by asking whether your code runs after the response, not by comparing free-tier tables that expire.