How to Fix 'async event loop error when scaling' in AutoGen (TypeScript)
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
| Broken | Fixed |
|---|---|
| Shared singleton runtime/agent | Create per-request runtime/agent |
Concurrent .run() on same instance | One run per isolated instance |
| Module-level state | Request-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 exceededafter repeated retries - •
UnhandledPromiseRejectionWarningaroundagent.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
- •
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.
- •Log object identity for
- •
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.
- •
- •
- •
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.
- •Temporarily disable
- •
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
- •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