How to Fix 'async event loop error in production' in CrewAI (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
async-event-loop-error-in-productioncrewaitypescript

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

BrokenFixed
Reuses a singleton crew instance across requestsCreates a fresh crew per job/request
Calls kickoff() without isolating async contextWraps execution in a dedicated async function
Lets multiple requests share the same mutable statePasses 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

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

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

Related Guides