How to Fix 'async event loop error in production' in CrewAI (TypeScript)
What the error means
If you’re seeing async event loop error in production with CrewAI in TypeScript, you’re usually dealing with a runtime mismatch: some async work is being triggered on a loop that’s already running, closed, or being reused incorrectly across requests. In practice, this shows up when agents/tools are called from server handlers, background jobs, or worker processes that manage their own concurrency.
The stack trace often points at Crew.kickoff(), tool execution, or a wrapper around Promise/EventEmitter usage. The real issue is usually not CrewAI itself — it’s how the app is starting and reusing async execution.
The Most Common Cause
The #1 cause is mixing top-level async orchestration with request-scoped execution, then accidentally calling CrewAI multiple times on the same lifecycle boundary. In TypeScript apps, this usually happens in Express, Next.js route handlers, serverless functions, or queue workers.
Broken pattern vs fixed pattern
| Broken | Fixed |
|---|---|
| Reuses a singleton crew instance across requests | Creates a fresh crew per job/request |
Calls kickoff() without isolating async context | Wraps execution in a dedicated async function |
| Lets multiple requests share the same mutable state | Passes input explicitly and keeps state local |
// broken.ts
import { Crew } from "@crewai/typescript";
import { researchAgent } from "./agents";
const crew = new Crew({
agents: [researchAgent],
tasks: [],
});
export async function handler(req: Request) {
// Often blows up under load with:
// "Error: async event loop error in production"
// or a lower-level runtime error around kickoff/tool execution
const result = await crew.kickoff({
topic: await req.json(),
});
return Response.json(result);
}
// fixed.ts
import { Crew } from "@crewai/typescript";
import { researchAgent } from "./agents";
function buildCrew() {
return new Crew({
agents: [researchAgent],
tasks: [],
});
}
export async function handler(req: Request) {
const body = await req.json();
const crew = buildCrew();
const result = await crew.kickoff({
topic: body.topic,
});
return Response.json(result);
}
The difference is simple: don’t keep one long-lived Crew instance around if it holds execution state. Build it inside the request/job boundary so each invocation gets a clean async context.
Other Possible Causes
1) Calling kickoff() inside an unawaited callback
This creates overlapping executions and makes failures look random.
// broken
queue.on("message", (msg) => {
crew.kickoff({ topic: msg.topic }); // not awaited
});
// fixed
queue.on("message", async (msg) => {
await crew.kickoff({ topic: msg.topic });
});
2) Running CrewAI inside a closed or invalid worker context
If your worker exits before the task finishes, the event loop gets torn down mid-flight.
// broken
processJob().then(() => process.exit(0));
// fixed
await processJob();
// let the runtime exit naturally after all promises settle
If you’re using BullMQ, Cloudflare Workers, or serverless functions, make sure the platform supports long-running async work before invoking Crew.kickoff().
3) Tool code opens its own event loop resources incorrectly
A custom tool that starts timers, sockets, or child processes can trigger loop-related failures during agent execution.
// broken tool
export class BadTool {
async run(input: string) {
setInterval(() => console.log(input), 1000);
return "done";
}
}
// fixed tool
export class GoodTool {
async run(input: string) {
try {
return `processed ${input}`;
} finally {
// clean up any resources here if you created them
}
}
}
Look at custom tools first if the stack trace points to Tool.execute, run, or adapter code.
4) Mixing ESM/CJS or incompatible Node runtime versions
CrewAI TypeScript code can behave differently depending on Node version and module resolution.
{
"type": "module",
"engines": {
"node": ">=20"
}
}
Common failure mode:
- •Local dev on Node 22 works
- •Production runs Node 18 or an older Lambda runtime
- •Async behavior changes under load
Keep your local and production Node versions aligned.
How to Debug It
- •
Find the first real stack frame
- •Don’t stop at
async event loop error in production. - •Look for the first frame in your codebase near
crew.kickoff(), custom tools, or request handlers.
- •Don’t stop at
- •
Check whether the crew instance is shared
- •Search for module-level singletons:
const crew = new Crew(...) - •If that exists outside a function, move it inside your job/request handler.
- •Search for module-level singletons:
- •
Isolate tool execution
- •Temporarily replace all custom tools with no-op implementations.
- •If the error disappears, one of your tools is leaking timers, sockets, child processes, or unhandled promises.
- •
Run the same path under production-like concurrency
- •Use
autocannon, k6, or parallel test jobs. - •Many of these errors only appear when two requests hit the same mutable state at once.
- •Use
Prevention
- •Create crews per request/job unless you have proven they are stateless.
- •Keep custom tools pure and short-lived; clean up every resource they open.
- •Match Node versions between local development and production.
- •Always
await crew.kickoff()and avoid fire-and-forget calls in handlers. - •Add integration tests that run multiple concurrent invocations against the same endpoint.
If you want one rule to remember: treat CrewAI execution like database transactions — scoped, isolated, and fully awaited. That removes most of these production-only event loop failures before they ever hit logs.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit