
A crew that works in a notebook and fails in production is usually failing on state, not on prompting. This covers checkpointer design, at-least-once delivery and the duplicate-order problem, token budgets per run, and the four numbers worth alerting on.
A crew of four agents writes a working prototype in an afternoon. Three weeks later it is in production, and it starts producing duplicate purchase orders. The prompts did not change. What changed is that the worker restarted mid-run, the queue delivered the same job twice, and nobody had decided what a retry should mean.
That failure pattern is the real work in multi-agent systems. Framework choice matters less than most teams expect. State, delivery semantics, and cost control decide whether the thing survives a Monday.
Where serverless stops fitting
Serverless runtimes cap how long one invocation may run, and those caps are measured in minutes. An agent that researches, writes, tests, reads its own test output and revises will happily use forty. When the platform kills the invocation, the work is gone unless you wrote it down.
Three other properties bite at the same time. There is no local disk worth relying on, so intermediate artefacts have to go somewhere durable. Importing PyTorch, a vector library and your own package takes seconds of cold start, which is the difference between a snappy agent and a timeout. And any in-process memory evaporates when the container is recycled, so a conversational agent forgets the ticket it was solving.
A VPS removes all four constraints at once. It also removes the platform that was hiding your operational gaps, which is why the rest of this article is about design rather than hosting.
CrewAI or LangGraph: choose by failure mode
The two frameworks answer different questions. CrewAI asks who does the work. LangGraph asks what happens next.
CrewAI models a team: agents with roles, goals and backstories, tasks handed off in sequence or through a manager agent that delegates and reviews. It is quick to stand up and pleasant to read. The failure mode is unpredictability. When a manager agent decides to reorder work or ask for a rewrite, cost and latency move in ways you did not plan for.
LangGraph models a state machine: nodes that do work, edges that decide the next node, cycles that are explicit in the graph. Its failure mode is upfront effort. You must define state, routes and exit conditions before anything runs, and a badly designed graph loops forever with perfect determinism.
Question | CrewAI | LangGraph |
|---|---|---|
Unit of design | Role and task | Node and edge |
Control over flow | Delegated to the crew | Written by you |
State between steps | Memory components | Checkpointer, per thread |
Human approval | Task-level hooks | Interrupt and resume |
Best first workload | Research and drafting pipelines | Code, audit and reconciliation flows |
Common production failure | Runaway delegation | Infinite cycle |
Plenty of teams run both: a LangGraph graph for the deterministic outer flow, with a CrewAI crew called as one node for the messy research step.
State is the part you will rewrite twice
LangGraph separates two kinds of persistence, and mixing them up is the usual first mistake. A checkpointer stores a thread's graph state as checkpoints: the message list, the variables, the outputs of each node. A store holds application data across threads, such as a customer's preferences or a shared knowledge base [1].
Checkpointing gives you three things for free: resumption after a crash, time travel to an earlier step, and human-in-the-loop pauses that survive a restart. It also creates three obligations.
The first is thread identity. The thread_id is written into a column with a length limit, so keep it under 255 characters and use a UUID rather than a sentence. The second is durability. In-memory savers lose every checkpoint when the process dies, which is exactly the moment you need them [2]. Use the Postgres saver in anything that runs for money.
The third is growth. Checkpoints accumulate on long conversations, and storage cost plus restore latency grow with them, so a retention job is part of the design rather than an afterthought.
Subgraphs deserve their own paragraph. Each subgraph keeps its own checkpoint namespace, so a parent graph may not see a child's writes immediately; data that must cross graph boundaries belongs in a store, not in nested state. This one bites during review, when two engineers disagree about what the state contained.
The stack that carries it
request / cron
β
βΌ
FastAPI gateway βββΊ Postgres (threads, checkpoints, job rows)
β β²
β enqueue β checkpoint writes
βΌ β
Redis queue βββΊ worker daemon βββΊ graph or crew
β
βββΊ sandboxed tool container (docker run, no net)
βββΊ LLM API or local Ollama
The gateway does one thing: accept a request, create a job row, push an id, return the id. It never waits for the agent. Everything after that is asynchronous, which is what makes a forty-minute run compatible with an HTTP client that gives up after thirty seconds.
The worker is a long-lived process, not a function. Both roles fit on one cloud hosting instance for a small team; split them once the worker starts saturating disk. It pulls a job, loads state by thread_id, runs the graph until it ends or hits an interrupt, and writes the result back. Redis carries job ids, never payloads: a 2 MB prompt in a queue message is a memory problem waiting to happen. The Postgres image ships the pgvector extension, so embeddings sit next to the checkpoint tables instead of in a second database [5].
services:
postgres:
image: pgvector/pgvector:pg16
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB}
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
retries: 10
networks: [agents]
redis:
image: redis:7-alpine
restart: always
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?set REDIS_PASSWORD}"]
volumes: ["redisdata:/data"]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
retries: 10
networks: [agents]
gateway:
build: {context: ., dockerfile: Dockerfile.api}
restart: always
ports: ["127.0.0.1:8000:8000"]
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY}
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
networks: [agents]
worker:
build: {context: ., dockerfile: Dockerfile.worker}
restart: always
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
MAX_CONCURRENT_AGENTS: "4"
RUN_TOKEN_BUDGET: "250000"
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
networks: [agents]
volumes: {pgdata: {}, redisdata: {}}
networks:
agents:
driver: bridgeThe worker has no volume mount for the Docker socket. An agent that can talk to the host daemon can start a privileged container, which is root on the host with extra steps. If tools need containers, run a dedicated runner service with a narrow API and no host socket [6].
Retries, idempotency, and the duplicate purchase order
Queues deliver at least once. A worker that dies after calling the supplier API but before acknowledging the job will see that job again, and a naive retry buys the stock twice [4].
The fix is boring and it works, and it is the same discipline that keeps AI-built code from reaching production without guardrails. Every job carries an idempotency key derived from the business event, not from the run: po-2026-10-22-4471, not run-8f3a. Every mutating tool call writes that key to a tool_calls table with a unique index before it goes out, and the next attempt reads the row instead of repeating the call.
tool_calls(
idempotency_key text primary key,
tool text,
request_hash text,
response jsonb,
completed_at timestamptz
)
Two more rules keep retries sane. Retry only what is safe: a timeouts on a read is different from a timeout on a payment. And cap attempts per run, then park the job in a dead-letter queue with its checkpoint intact, so a human resumes from the last good step instead of replaying an hour of model calls.
Cost control that does not depend on hope
Agent costs surprise people because they are unbounded by design. A loop that improves output also loops more. Four controls cover most of it.
A hard token budget per run. Pass it into the worker, count prompt plus completion tokens per node, and stop the graph when the budget is spent. A run that dies at the budget with a partial result is cheaper than a run that finishes at four times the estimate.
Model tiering per node. Extraction, classification and formatting do not need a frontier model. Route them to a small model, and reserve the expensive one for planning and review. On a graph with ten nodes, this single change usually moves the bill more than any prompt edit.
Prompt cache discipline. Keep the static prefix stable: same system prompt, same tool list, same ordering. A cache hit is a fraction of an input token's price, and a prefix that changes per run gets none of it.
Per-tenant quotas. One customer's runaway cron should not consume the month for everyone else. A token counter per tenant with a hard stop converts a finance incident into a support ticket.
What to log, and the four numbers to alert on
Instrument per node, not per run. Each row should carry thread_id, node name, start and end timestamps, tokens in and out, tool calls made, and whether the node was a retry. That table answers the two questions you will actually ask: which step is slow, and which step spends.
Alert on four things. Queue depth rising while workers sit idle, which means a dependency is hanging. Runs ending at the budget, which means a loop is not converging. Duplicate idempotency keys hit, which means delivery semantics are doing what they were designed to do and something upstream is flapping. And cost per completed job, trailing seven days, which catches quiet regressions that no latency graph will show.
Sizing, briefly
Agents are memory-bound and bursty. Budget 2 GB per concurrent crew for Python plus framework overhead, add 4 GB if you run local embeddings or a small model, and keep 25% of the box free so a batch does not trigger the OOM killer mid-run. An AI agent hosting plan sized for the worker rather than the web tier saves a migration later. NVMe matters because every node transition writes a checkpoint; on slow storage those writes queue behind each other and the graph stalls for reasons that look like model latency.
Agent workloads need a box that stays up
AI agent hosting with persistent NVMe storage, a private network for Postgres and Redis, and engineers who will read your checkpoint table with you.
See SysopHost AI agent hostingComments
No comments yet. Be the first to comment!
Manpreet Singh