How to Fix 'callback not firing when scaling' in LangChain (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
callback-not-firing-when-scalinglangchaintypescript

When callback not firing when scaling shows up in LangChain TypeScript, it usually means your callback handler works in local tests but stops being invoked once you add concurrency, batching, or a second execution path. In practice, this is almost always a wiring problem: the callback is attached to the wrong object, lost across async boundaries, or never passed into the runnable that actually executes.

The symptom is annoying because the chain still returns output. The only thing missing is the side effect you expected from BaseCallbackHandler, handleLLMEnd, handleChainEnd, or custom event hooks.

The Most Common Cause

The #1 cause is attaching callbacks to a chain instance, then invoking a different runnable path during scaling. This happens a lot when people move from chain.call() to RunnableSequence, batch(), map(), or worker-based processing and assume inherited callbacks will follow automatically.

Here’s the broken pattern:

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { CallbackManager } from "@langchain/core/callbacks/manager";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });

const prompt = PromptTemplate.fromTemplate("Summarize: {text}");
const parser = new StringOutputParser();

const chain = prompt.pipe(llm).pipe(parser);

const callbackManager = CallbackManager.fromHandlers({
  handleLLMEnd(output) {
    console.log("LLM finished:", output);
  },
});

// Looks fine, but this does not guarantee propagation everywhere
await chain.invoke(
  { text: "Some long document" },
  { callbacks: callbackManager.getHandlers() }
);

// Later, scaled path:
await Promise.all([
  chain.invoke({ text: "A" }),
  chain.invoke({ text: "B" }),
]);

And here’s the fixed pattern:

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";

class LoggingHandler extends BaseCallbackHandler {
  name = "logging_handler";

  handleLLMEnd(output: any) {
    console.log("LLM finished:", output);
  }
}

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const prompt = PromptTemplate.fromTemplate("Summarize: {text}");
const parser = new StringOutputParser();

const chain = prompt.pipe(llm).pipe(parser);

await Promise.all([
  chain.invoke({ text: "A" }, { callbacks: [new LoggingHandler()] }),
  chain.invoke({ text: "B" }, { callbacks: [new LoggingHandler()] }),
]);

The important part is this:

  • pass callbacks at the actual invocation site
  • do not assume .withConfig() or a parent wrapper will survive every execution path
  • create per-request handler instances if you need request-scoped state

If you’re using batch(), be careful too. Some code paths call internal runnables directly, and your handler never sees the event unless it’s attached to the runnable that emits it.

Other Possible Causes

1. Using the wrong callback hook for the object you’re observing

handleLLMEnd() only fires for LLM runs. If you’re expecting tool or chain events, use the matching hook.

class MyHandler extends BaseCallbackHandler {
  handleChainEnd(output: any) {
    console.log("Chain ended");
  }

  handleToolEnd(output: any) {
    console.log("Tool ended");
  }

  handleLLMEnd(output: any) {
    console.log("LLM ended");
  }
}

If you only implemented handleStart() and waited for everything else to route through it, nothing will happen.

2. Reusing one handler instance across concurrent requests

This breaks when your handler stores mutable state like counters, request IDs, or buffers.

class StatefulHandler extends BaseCallbackHandler {
  runs = [];

  handleLLMStart(_llm: any, _prompts: string[], runId: string) {
    this.runs.push(runId);
  }
}

Fix it by creating a fresh handler per request:

await Promise.all(inputs.map((input) =>
  chain.invoke(input, { callbacks: [new StatefulHandler()] })
));

3. Missing await on async callback logic

If your handler writes to Redis, Postgres, or an HTTP endpoint and you forget to await it inside the hook, Node can exit early under load.

class WebhookHandler extends BaseCallbackHandler {
  async handleLLMEnd(output: any) {
    await fetch(process.env.WEBHOOK_URL!, {
      method: "POST",
      body: JSON.stringify(output),
    });
  }
}

Also make sure your app waits for the chain promise itself:

await chain.invoke(input, { callbacks: [new WebhookHandler()] });

4. Version mismatch between LangChain packages

This shows up as silent no-op behavior or weird type compatibility issues after upgrades.

Common mismatch pattern:

  • @langchain/core at one version
  • @langchain/openai at another
  • old imports from deprecated packages

Check that these are aligned:

{
  "dependencies": {
    "@langchain/core": "^0.3.x",
    "@langchain/openai": "^0.3.x"
  }
}

If you mix old and new callback APIs, events may not propagate as expected.

How to Debug It

  1. Confirm which event should fire

    • If you expect handleLLMEnd, verify an actual LLM call happened.
    • If the step is a retriever or tool, use handleRetrieverEnd or handleToolEnd.
  2. Add all major hooks

    • Implement handleChainStart, handleLLMStart, handleLLMEnd, and handleChainEnd.
    • Log runId and input shape so you can see where execution stops.
  3. Test with a single synchronous path

    • Run one .invoke() call before using .batch() or Promise.all().
    • If it works once but fails under concurrency, suspect shared state or lost config propagation.
  4. Inspect where callbacks are attached

    • Attach them directly on .invoke(input, { callbacks }).
    • If you use .withConfig({ callbacks }), confirm that every downstream runnable inherits it.

Prevention

  • Pass callbacks at the leaf invocation point, not just on a parent wrapper.
  • Keep handlers stateless unless you explicitly scope them per request.
  • Align LangChain package versions and avoid mixing old callback APIs with new runnable patterns.
  • Add one integration test that runs the same chain through:
    • single invoke
    • concurrent invoke
    • batch mode

That test catches most “works locally, breaks when scaling” callback bugs before they hit production.


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