← Back to DevBytes

Streaming Responses in Production with CrewAI: Complete Guide

Streaming Responses in Production with CrewAI: Complete Guide

Building AI agents that feel responsive is one of the most important UX decisions you can make in production. When a CrewAI crew runs a complex multi-step task, users can wait tens of seconds before seeing any output. Streaming responses solves this by delivering tokens to the client as they're generated, dramatically improving perceived performance. This guide walks through everything you need to know to ship streaming in production.

What Is Response Streaming in CrewAI?

Streaming, in the context of LLM applications, means forwarding model output to the end user incrementally as it is produced, rather than buffering the entire response and returning it all at once. CrewAI sits on top of LiteLLM, which itself wraps provider SDKs like OpenAI's, Anthropic's, and others. Each of these providers supports Server-Sent Events (SSE) or chunked HTTP responses that emit tokens one piece at a time.

CrewAI exposes this capability through the stream parameter on its LLM class and through the kickoff / kickoff_async interfaces on Crew. When streaming is enabled, the crew yields events as agents produce output, instead of returning a single final result object.

Why Streaming Matters in Production

How to Enable Streaming in CrewAI

The simplest way to enable streaming is to configure your LLM instance with stream=True and then iterate over the output of crew.kickoff(). Let's look at a complete example.

from crewai import Agent, Task, Crew, Process
from crewai.llm import LLM

# Configure an LLM with streaming enabled
streaming_llm = LLM(
    model="gpt-4o-mini",
    stream=True,
    temperature=0.7,
)

researcher = Agent(
    role="Senior Research Analyst",
    goal="Produce a concise briefing on the given topic",
    backstory="You are an expert at distilling complex information.",
    llm=streaming_llm,
    verbose=True,
)

writer = Agent(
    role="Technical Writer",
    goal="Turn the briefing into a polished article",
    backstory="You write clear, engaging technical content.",
    llm=streaming_llm,
)

research_task = Task(
    description="Research the topic: {topic}",
    expected_output="A 3-paragraph briefing with key facts.",
    agent=researcher,
)

write_task = Task(
    description="Write a polished article based on the briefing.",
    expected_output="A markdown article of 500-800 words.",
    agent=writer,
)

crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, write_task],
    process=Process.sequential,
)

# Kick off with streaming
for chunk in crew.kickoff(inputs={"topic": "edge computing"}):
    print(chunk, end="", flush=True)

When stream=True is set on the LLM, CrewAI's kickoff returns a generator. Each yielded item is a chunk of text produced by the active agent. The final result is also accessible via the CrewOutput object returned when the generator is exhausted.

Working with Async Streaming

Production web servers like FastAPI are async by default. CrewAI provides kickoff_async which returns an async generator. This is the recommended path for any service handling concurrent requests.

import asyncio
from crewai import Agent, Task, Crew, Process
from crewai.llm import LLM

streaming_llm = LLM(model="gpt-4o-mini", stream=True)

analyst = Agent(
    role="Data Analyst",
    goal="Analyze the provided metrics and summarize trends.",
    backstory="You are precise and concise.",
    llm=streaming_llm,
)

task = Task(
    description="Analyze these metrics: {metrics}",
    expected_output="A short summary of trends.",
    agent=analyst,
)

crew = Crew(agents=[analyst], tasks=[task], process=Process.sequential)

async def run_stream():
    async for chunk in crew.kickoff_async(
        inputs={"metrics": "revenue: 1.2M, churn: 3.2%, nps: 47"}
    ):
        print(chunk, end="", flush=True)

asyncio.run(run_stream())

Exposing Streaming Over HTTP with FastAPI

The most common production pattern is to expose the crew behind a FastAPI endpoint that returns a StreamingResponse using Server-Sent Events. This is what browser frontends and most LLM UI libraries expect.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import json
import asyncio

from crewai import Agent, Task, Crew
from crewai.llm import LLM

app = FastAPI()

streaming_llm = LLM(model="gpt-4o-mini", stream=True)

agent = Agent(
    role="Assistant",
    goal="Answer the user's question helpfully.",
    backstory="You are a knowledgeable assistant.",
    llm=streaming_llm,
)

task = Task(
    description="Answer this question: {question}",
    expected_output="A helpful answer.",
    agent=agent,
)

crew = Crew(agents=[agent], tasks=[task])

class QueryRequest(BaseModel):
    question: str

async def event_generator(question: str):
    async for chunk in crew.kickoff_async(inputs={"question": question}):
        # SSE format: each event is "data: <payload>\n\n"
        payload = json.dumps({"token": str(chunk)})
        yield f"data: {payload}\n\n"
    yield "data: [DONE]\n\n"

@app.post("/chat")
async def chat(req: QueryRequest):
    return StreamingResponse(
        event_generator(req.question),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",  # disable nginx buffering
        },
    )

The X-Accel-Buffering: no header is critical if you're behind nginx — without it, nginx will buffer the entire response and your streaming will appear to do nothing until the crew finishes.

Handling Multi-Agent Crews

With sequential crews containing multiple agents, streaming yields output from whichever agent is currently active. You may want to distinguish which agent produced each chunk so the frontend can render agent boundaries. CrewAI emits structured events you can inspect.

from crewai.utilities.events import event_listener
from crewai.utilities.events.base_events import CrewKickoffStartedEvent
from crewai.utilities.events.crew_events import (
    AgentExecutionStartedEvent,
    AgentExecutionCompletedEvent,
)

@event_listener(AgentExecutionStartedEvent)
def on_agent_start(e: AgentExecutionStartedEvent):
    print(f"\n--- Agent starting: {e.agent.role} ---")

@event_listener(AgentExecutionCompletedEvent)
def on_agent_done(e: AgentExecutionCompletedEvent):
    print(f"\n--- Agent finished: {e.agent.role} ---")

You can use these listeners to inject SSE markers into your stream so the client knows when one agent hands off to the next. For example, emit a data: {"type":"agent_start","agent":"Researcher"} event before forwarding tokens, and a data: {"type":"agent_end"} event when the listener fires for completion.

Best Practices for Production Streaming

1. Always set timeouts and backpressure handling. Streaming connections can hang if a client disconnects mid-stream. Use FastAPI's Request.is_disconnected() check inside your generator and break out early.

from fastapi import Request

async def event_generator(question: str, request: Request):
    async for chunk in crew.kickoff_async(inputs={"question": question}):
        if await request.is_disconnected():
            break
        yield f"data: {json.dumps({'token': str(chunk)})}\n\n"
    yield "data: [DONE]\n\n"

@app.post("/chat")
async def chat(req: QueryRequest, request: Request):
    return StreamingResponse(
        event_generator(req.question, request),
        media_type="text/event-stream",
    )

2. Disable buffering at every layer. Streaming breaks if any intermediary buffers. Check nginx (proxy_buffering off), Cloudflare (disable response buffering or use WebSockets), and any CDN in front of your API.

3. Use async throughout. Mixing sync kickoff with an async web server blocks the event loop and tanks throughput. Always use kickoff_async in async frameworks.

4. Cap concurrency. Streaming requests hold connections open much longer than typical REST calls. Use a semaphore or queue to limit concurrent crews and protect your LLM provider rate limits.

import asyncio

MAX_CONCURRENT_CREWS = 10
semaphore = asyncio.Semaphore(MAX_CONCURRENT_CREWS)

async def event_generator(question: str, request: Request):
    async with semaphore:
        async for chunk in crew.kickoff_async(inputs={"question": question}):
            if await request.is_disconnected():
                break
            yield f"data: {json.dumps({'token': str(chunk)})}\n\n"
    yield "data: [DONE]\n\n"

5. Log structured events. Streamed chunks are easy to lose. Log agent start/end events, token counts, and errors to your observability stack so you can reconstruct runs after the fact.

6. Handle provider-specific quirks. Not all providers stream identically. Anthropic streams include message_start, content_block_delta, and message_stop events. LiteLLM normalizes most of this, but test each provider you support. Some models (especially via certain gateways) silently ignore stream=True — verify with a quick smoke test.

7. Provide a non-streaming fallback. Some clients (CLI tools, batch jobs) prefer a single JSON response. Offer both /chat (streaming) and /chat/sync (blocking) endpoints sharing the same crew definition.

8. Validate inputs before streaming starts. Once you've sent the SSE headers and started yielding, you can't easily return a 400. Validate the request body, authenticate the user, and check rate limits before invoking the crew.

Consuming the Stream from a Frontend

On the client side, use the browser's EventSource API or a library like fetch-event-source (which supports POST requests, unlike native EventSource).

import { fetchEventSource } from '@microsoft/fetch-event-source';

async function streamChat(question, onToken, onDone) {
  await fetchEventSource('/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question }),
    onmessage(ev) {
      if (ev.data === '[DONE]') {
        onDone();
        return;
      }
      const payload = JSON.parse(ev.data);
      if (payload.token) onToken(payload.token);
    },
  });
}

// Usage
streamChat(
  'Explain edge computing',
  (token) => document.getElementById('output').textContent += token,
  () => console.log('stream complete')
);

Conclusion

Streaming transforms CrewAI from a batch-processing library into a real-time, user-facing system. By enabling stream=True on your LLM, using kickoff_async in async servers, exposing the output through FastAPI's StreamingResponse with SSE, and following production best practices around buffering, concurrency, and disconnect handling, you can ship multi-agent experiences that feel instant and reliable. Start with a single-agent crew to validate your pipeline end-to-end, then layer in multi-agent event markers and observability as your application grows. The result is a noticeably better user experience and a more robust service that handles long-running agent workflows without timeouts or silent failures.

— Ad —

Google AdSense will appear here after approval

← Back to all articles