How to Fix 'async event loop error when scaling' in AutoGen (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
async-event-loop-error-when-scalingautogentypescript

When you see async event loop error when scaling in AutoGen TypeScript, it usually means you’re trying to run async agent work inside a lifecycle that already has an active event loop, or you’re starting multiple runs against the same runtime/state. It shows up most often when moving from a single local test to parallel workers, API routes, queue consumers, or serverless handlers.

The fix is usually not “make it more async.” It’s to stop nesting runs, isolate per-request state, and make sure your AutoGen runtime is created and disposed in the right place.

The Most Common Cause

The #1 cause is reusing one AutoGen runtime or agent instance across concurrent requests. In TypeScript, this often happens when you keep a singleton AssistantAgent, RoundRobinGroupChat, or SingleThreadedAgentRuntime in module scope and then call it from multiple requests at once.

That works in a toy script. It breaks when you scale because two invocations try to drive the same internal async loop.

Broken pattern vs fixed pattern

BrokenFixed
Shared singleton runtime/agentCreate per-request runtime/agent
Concurrent .run() on same instanceOne run per isolated instance
Module-level stateRequest-scoped factory
// BROKEN: shared state across requests
import { AssistantAgent, SingleThreadedAgentRuntime } from "@autogen/core";

const runtime = new SingleThreadedAgentRuntime();
const agent = new AssistantAgent({
  name: "support-agent",
  modelClient,
});

export async function handler(req: Request) {
  // Multiple requests can hit this at once
  const result = await agent.run("Summarize this claim");
  return Response.json({ result });
}
// FIXED: create isolated instances per request
import { AssistantAgent, SingleThreadedAgentRuntime } from "@autogen/core";

export async function handler(req: Request) {
  const runtime = new SingleThreadedAgentRuntime();
  const agent = new AssistantAgent({
    name: "support-agent",
    modelClient,
  });

  try {
    const result = await agent.run("Summarize this claim");
    return Response.json({ result });
  } finally {
    // If your runtime exposes cleanup/shutdown, call it here.
    // await runtime.close();
  }
}

If you’re using group chats, the same rule applies:

// BROKEN
const team = new RoundRobinGroupChat([agentA, agentB]);

app.post("/run", async (req) => {
  const output = await team.run("Handle this policy question");
  return Response.json(output);
});
// FIXED
app.post("/run", async (req) => {
  const agentA = new AssistantAgent({ name: "a", modelClient });
  const agentB = new AssistantAgent({ name: "b", modelClient });
  const team = new RoundRobinGroupChat([agentA, agentB]);

  const output = await team.run("Handle this policy question");
  return Response.json(output);
});

If the error message includes something like:

  • Error: Cannot run while another task is active
  • Error: event loop already running
  • RangeError: Maximum call stack size exceeded after repeated retries
  • UnhandledPromiseRejectionWarning around agent.run()

you’re likely sharing execution state.

Other Possible Causes

1) Nested await inside an event callback that re-enters the same flow

This happens when a message handler calls back into the same chat/session before the first run finishes.

// BROKEN
team.onMessage(async (msg) => {
  if (msg.type === "tool_call") {
    await team.run("Continue"); // re-enters active loop
  }
});

Fix it by queueing follow-up work outside the callback.

// FIXED
team.onMessage((msg) => {
  if (msg.type === "tool_call") {
    void queueMicrotask(async () => {
      await followUp(msg);
    });
  }
});

2) Parallel .run() calls on one conversation/session

This is common when you do Promise.all() over the same agent or thread.

// BROKEN
await Promise.all([
  agent.run("Task A"),
  agent.run("Task B"),
]);

Use separate agents/sessions or serialize access.

// FIXED
const r1 = await agent.run("Task A");
const r2 = await agent.run("Task B");

If parallelism is required:

const makeAgent = () => new AssistantAgent({ name: crypto.randomUUID(), modelClient });

await Promise.all([
  makeAgent().run("Task A"),
  makeAgent().run("Task B"),
]);

3) Mixing Node timers/events with an unhandled long-running loop

AutoGen can keep internal work alive longer than expected if you forget to stop polling or cancel background tasks.

// BROKEN
setInterval(async () => {
  await agent.run("Poll inbox");
}, 1000);

Use explicit cancellation and avoid overlapping intervals.

let running = false;

setInterval(async () => {
  if (running) return;
  running = true;
  try {
    await agent.run("Poll inbox");
  } finally {
    running = false;
  }
}, 1000);

4) Running under serverless or hot-reload with stale module state

Next.js dev mode, Vercel functions, and similar environments can preserve module scope longer than you expect.

// BROKEN: module-scope singleton survives reloads/invocations
export const assistant = new AssistantAgent({ name: "support", modelClient });

Move initialization into the handler or use a factory keyed by request/session ID.

export function createAssistant() {
  return new AssistantAgent({ name: "support", modelClient });
}

How to Debug It

  1. Check whether the same instance is reused

    • Log object identity for AssistantAgent, RoundRobinGroupChat, and runtime objects.
    • If two requests share the same instance ID, that’s your bug.
  2. Look for overlapping runs

    • Add logs before and after every .run() call.

    • If you see:

      • start run A
      • start run B
      • end run A

      then you have concurrent access to one session.

  3. Remove all callbacks and timers

    • Temporarily disable setInterval, event listeners, tool callbacks, and nested follow-up logic.
    • If the error disappears, reintroduce them one by one until it returns.
  4. Run a single-request repro

    • Hit one endpoint once with no retries and no concurrency.
    • If single-run works but load testing fails, your issue is almost certainly shared state or re-entrancy.

Prevention

  • Create AutoGen agents and runtimes per request/session unless you have explicit concurrency control.
  • Never call .run() twice at the same time on the same conversation object.
  • Wrap all background loops with cancellation guards and cleanup logic.
  • In web apps, avoid module-level singletons for anything that owns chat state or async execution flow.

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