Multi-agent frameworks went from research toys to production tools fast, and CrewAI (https://docs.crewai.com) is one of the most approachable: you define agents with roles and goals, give them tasks, and the crew collaborates to produce a result. Writing the crew is the fun part. Deploying it — so a real app can call it over HTTP without timing out on a five-minute agent run — is the part that trips people up. I run PandaStack; here's the deployment pattern that actually holds up, including the async detail most tutorials skip.
A minimal crew
# crew.py
from crewai import Agent, Task, Crew
researcher = Agent(
role="Researcher",
goal="Find accurate, current facts about the topic",
backstory="A meticulous analyst who never guesses.",
verbose=False,
)
writer = Agent(
role="Writer",
goal="Turn research into a clear, concise summary",
backstory="A former editor who hates filler.",
verbose=False,
)
def run_crew(topic: str) -> str:
research = Task(description=f"Research: {topic}", agent=researcher, expected_output="Bullet-point findings")
summary = Task(description="Write a 150-word summary from the findings", agent=writer, expected_output="A tight summary")
crew = Crew(agents=[researcher, writer], tasks=[research, summary])
return str(crew.kickoff())CrewAI needs an LLM. Point it at your provider via env vars (an OPENAI_API_KEY, or better, your self-hosted LiteLLM proxy so you control cost and keys).
The deployment problem: crews are slow
A crew run can take seconds to minutes — multiple LLM calls, tool use, back-and-forth. If you expose it as a *synchronous* HTTP endpoint, the request hangs, clients time out, and you can't tell a slow run from a stuck one. The fix is a job pattern: accept the request, return a job ID immediately, run the crew in the background, and let the client poll for the result.
Step 1: FastAPI with a background job
# main.py
import os, uuid
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
from crew import run_crew
app = FastAPI()
JOBS: dict[str, dict] = {} # swap for Redis/Postgres in real use
class Req(BaseModel):
topic: str
def _run(job_id: str, topic: str):
try:
JOBS[job_id] = {"status": "done", "result": run_crew(topic)}
except Exception as e:
JOBS[job_id] = {"status": "error", "error": str(e)}
@app.post("/crew")
def start(req: Req, bg: BackgroundTasks):
job_id = str(uuid.uuid4())
JOBS[job_id] = {"status": "running"}
bg.add_task(_run, job_id, req.topic)
return {"job_id": job_id}
@app.get("/crew/{job_id}")
def status(job_id: str):
return JOBS.get(job_id, {"status": "unknown"})
@app.get("/health")
def health():
return {"ok": True}The client POSTs a topic, gets a job_id instantly, then GETs the status until it's done. No hanging requests, no timeouts. For anything beyond a demo, back JOBS with managed Redis or Postgres so state survives restarts and scales across replicas.
Step 2: requirements and Dockerfile
crewai
fastapi
uvicorn[standard]
pydanticFROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Step 3: Deploy on PandaStack
- 1Push to Git.
- 2https://dashboard.pandastack.io → New App → connect repo. It detects the Dockerfile and deploys.
- 3Set environment variables: your LLM key (
OPENAI_API_KEY) or your LiteLLM proxy base URL. Encrypted, never in the repo. - 4Want durable job state? Databases → New Database → Redis (or PostgreSQL), attach it, and replace the in-memory
JOBSdict with reads/writes against it —DATABASE_URL/REDIS_URLare available as env vars. - 5Point your health check at
/healthand give it a generous timeout, since a busy instance running a crew shouldn't be killed mid-run.
CLI:
npm install -g @pandastack/cli
panda login
panda deployStep 4: Guardrails you'll want in production
- Cap concurrency. Each crew run fans out into several LLM calls; unbounded concurrency will nuke your provider rate limits and your bill. Limit how many jobs run at once.
- Set a per-job timeout. An agent stuck in a loop should fail, not run forever. Enforce a max duration in
_run. - Route LLM calls through LiteLLM. It gives you per-key budgets and fallbacks — invaluable when agents make unpredictable numbers of calls. (See the companion post on deploying a LiteLLM proxy.)
Honest tradeoffs
- Agent frameworks are non-deterministic and can be expensive. Costs vary run to run; budget and monitor. This is a CrewAI reality, not a hosting one.
- The background-job pattern adds moving parts (job store, polling) versus a simple sync endpoint — but a sync endpoint that times out isn't production, so it's necessary complexity.
- Free-tier apps scale to zero and cold-start; for an interactive agent API, keep it warm on a paid tier.
Wrap-up
CrewAI writes the multi-agent logic; the job-pattern FastAPI wrapper makes it survive contact with real HTTP clients. Deploy it on PandaStack, back the job state with managed Redis/Postgres, route LLM calls through LiteLLM, and cap concurrency and timeouts. That's a crew that ships.
Docs: https://docs.pandastack.io. Start free: https://dashboard.pandastack.io.