Skip to content

Step 6 · Wire it up the wrong way

Step 6 of 8The wrong way

Put the agent behind an HTTP endpoint the obvious way, and watch it fail.

You can be told that long requests time out. It doesn’t land. Watching your own browser hang for 45 seconds and then produce nothing does.

This is the version everyone writes first, including people who know better.

app/main.py
@app.post("/runs/naive")
async def create_run_naive(body: RunRequest):
run = await db.create_run(body.query, body.thread_id)
await research.run_agent(run["id"], body.query, run["thread_id"]) # ← THE BUG
return {"run": await db.get_run(run["id"]),
"steps": await db.get_steps(run["id"])}

That single await is the whole problem: the request cannot return until the agent finishes. Everything else on this page follows from it.

It’s clean, readable, and correct in the sense that it produces the right answer. It is also unshippable.

Start the server, from app/:

Terminal window
uv run fastapi dev main.py

Then start the client, in a second terminal, from client/:

Terminal window
npm run dev

Open http://localhost:5173. Leave the backend URL as http://localhost:8000, tick the “Naive mode” checkbox, and ask a question.

Now watch. And keep watching.

On localhost: it works. Forty-five seconds of nothing, then the answer appears all at once.

That’s the trap. It works perfectly on your machine, which is why people ship it.

Two things to notice:

  1. You had no idea whether it was working. No progress, no feedback, no way to tell a slow run from a crashed one. Try it and resist the urge to look at the terminal — that’s your user’s experience.
  2. Nothing between you and the server was impatient. There’s no proxy, no load balancer, no CDN on localhost. That’s the only reason it survived.

Deploy this and something in the chain gives up before your agent does:

Browser → CDN → Load balancer → Reverse proxy → Your server
↑ ↑ ↑
timeout timeout timeout

You’ll see:

504 Gateway Timeout

Yes — put a proxy in front with a short timeout:

Terminal window
# If you have nginx, or any proxy with a 30s read timeout:
# proxy_read_timeout 30s;

Or simulate it with curl, which is quicker:

Terminal window
curl -m 30 -X POST http://localhost:8000/runs/naive \
-H 'Content-Type: application/json' \
-d '{"query":"how do solar panels actually work?"}'
curl: (28) Operation timed out after 30001 milliseconds

Your server is still working. The client has already given up. That gap is the entire problem.

Sometimes you can, a bit. It still doesn’t work:

  • You don’t control every hop — corporate proxies, mobile carriers, CDNs.
  • A phone switching from Wi-Fi to mobile data drops the connection regardless.
  • You’re renting a request for 45 seconds. A few concurrent users and your free instance is out of workers; everyone queues, then everyone times out.
  • Even when it works, the user still stares at a spinner learning nothing.

Raising the timeout doesn’t remove the wall. It moves it, and makes failure slower and more expensive.

It returns instantly instead of hanging

You forgot to tick Naive mode, so you hit /runs — which is already the fixed version. Untick and retick, and check the request in your browser’s network tab: it should be POST /runs/naive.

The browser shows a CORS error instead of hanging

Check the Backend URL field actually matches where your server is running — a typo’d port produces a CORS error rather than a connection error, which sends people hunting in the wrong place. Locally, ALLOWED_ORIGINS defaults to *, so there is nothing to configure until you deploy.

npm run dev says the port is in use

Vite picks the next free port and prints it — read the URL it actually gives you rather than typing 5173 from memory.

It fails immediately with a 500

That’s a different problem — check the terminal. A missing key or a paused database will fail fast, and you want the slow failure here.

Now fix it. The change is smaller than you’d think.