How to Fix 'callback not firing during development' in LangGraph (Python)

By Cyprian AaronsUpdated 2026-04-21
callback-not-firing-during-developmentlanggraphpython

What this error usually means

If your LangGraph callback is not firing during development, the graph is usually running, but the callback handler is not attached to the execution path you think it is. In practice, this shows up when using stream(), invoke(), or async graph execution and expecting LangChain/LangGraph callbacks to print tokens, node events, or trace data.

The common pattern: everything works in production or in one code path, then during local dev you get no callback output at all. The graph completes, but your BaseCallbackHandler methods like on_chain_start, on_llm_new_token, or on_chain_end never fire.

The Most Common Cause

The #1 cause is passing callbacks in the wrong place.

In LangGraph, callbacks need to be attached to the runnable invocation context. If you pass them to a constructor, store them on the wrong object, or expect them to magically propagate through a custom node function, they will not fire.

Broken pattern vs fixed pattern

BrokenFixed
Callback passed to graph compile/build logic or ignored inside nodeCallback passed in config={"callbacks": [...]} at invocation time
Uses a plain Python function that bypasses LangChain run contextUses a Runnable/LLM call with config propagation
# BROKEN
from langchain.callbacks.base import BaseCallbackHandler
from langgraph.graph import StateGraph, END

class DebugHandler(BaseCallbackHandler):
    def on_llm_new_token(self, token: str, **kwargs):
        print("TOKEN:", token)

def call_model(state):
    # This may run, but callbacks won't fire if config isn't propagated
    response = llm.invoke(state["messages"])
    return {"messages": response}

graph = StateGraph(dict)
graph.add_node("model", call_model)
graph.set_entry_point("model")
graph.add_edge("model", END)

app = graph.compile()

# Wrong: callbacks are not attached here
result = app.invoke(
    {"messages": [{"role": "user", "content": "hello"}]}
)
# FIXED
from langchain.callbacks.base import BaseCallbackHandler
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END

class DebugHandler(BaseCallbackHandler):
    def on_llm_new_token(self, token: str, **kwargs):
        print("TOKEN:", token)

def call_model(state, config: RunnableConfig):
    response = llm.invoke(
        state["messages"],
        config=config  # propagate callback context
    )
    return {"messages": response}

graph = StateGraph(dict)
graph.add_node("model", call_model)
graph.set_entry_point("model")
graph.add_edge("model", END)

app = graph.compile()

result = app.invoke(
    {"messages": [{"role": "user", "content": "hello"}]},
    config={"callbacks": [DebugHandler()]}
)

If you are using stream() instead of invoke(), the same rule applies. Callbacks must be attached through the runtime config and propagated into any nested runnable calls.

A lot of developers miss this because LangGraph nodes are just Python callables. If your node does internal work without forwarding config, LangChain has no way to connect that work to your callback handler.

Other Possible Causes

1. You are using a sync callback with async execution

If your graph runs with ainvoke() or astream(), but your handler only implements sync methods incorrectly, you can get silent no-op behavior.

# Problematic for async paths if you're expecting async hooks
class DebugHandler(BaseCallbackHandler):
    def on_llm_new_token(self, token: str, **kwargs):
        print(token)

# Better for async-heavy workflows:
class AsyncDebugHandler(BaseCallbackHandler):
    async def on_llm_new_token(self, token: str, **kwargs):
        print(token)

2. Your node bypasses LangChain runnables entirely

If your node calls raw SDK code directly, callbacks from LangChain will not see it.

def bad_node(state):
    # Direct OpenAI/Anthropic SDK call here won't trigger LangChain callbacks
    text = client.responses.create(...)
    return {"answer": text}

Use a LangChain model wrapper or explicitly instrument the SDK yourself.

3. You forgot to pass config through nested nodes/tools

This is common in multi-step graphs.

def parent_node(state, config):
    child_result = child_runnable.invoke(state["query"])  # missing config
    return {"result": child_result}

Fix:

def parent_node(state, config):
    child_result = child_runnable.invoke(state["query"], config=config)
    return {"result": child_result}

4. Your environment hides output during development

Sometimes the callback fires, but your dev setup swallows stdout/stderr.

EnvironmentTypical issue
JupyterOutput buffering or cell execution order
DockerLogs not attached to container stdout
VS Code debuggerConsole routing hides prints

If your handler uses print(), switch to structured logging:

import logging

logger = logging.getLogger(__name__)

class DebugHandler(BaseCallbackHandler):
    def on_chain_start(self, serialized, inputs, **kwargs):
        logger.info("chain started: %s", serialized.get("name"))

How to Debug It

  1. Confirm the callback class is actually instantiated

    • Add a log in __init__ or right before passing it into config.
    • If you never see it constructed, you are debugging the wrong object.
  2. Check whether the node receives config

    • Add this inside your node:
      def my_node(state, config=None):
          print("CONFIG:", config)
      
    • If it is None, callbacks cannot propagate downstream.
  3. Verify the model/runnable call uses that same config

    • Compare these two:
      llm.invoke(messages)
      llm.invoke(messages, config=config)
      
    • The second one is what keeps LangChain tracing and callbacks alive.
  4. Turn on verbose tracing and watch for real event names

    • Look for events like:
      • on_chain_start
      • on_chain_end
      • on_llm_start
      • on_llm_new_token
    • If none appear, your callback chain is broken before execution reaches LangChain runtime.

Prevention

  • Always accept and forward config in every LangGraph node that calls another runnable.
  • Prefer LangChain wrappers over raw SDK calls when you need callback visibility.
  • Use one debugging handler early in development:
    class DebugHandler(BaseCallbackHandler):
        def on_chain_start(self, serialized, inputs, **kwargs): ...
        def on_llm_start(self, serialized, prompts, **kwargs): ...
        def on_llm_new_token(self, token: str, **kwargs): ...
        def on_chain_end(self, outputs, **kwargs): ...
    

If you standardize on passing config through every layer of your graph, this error mostly disappears. The moment you drop that propagation chain — especially inside custom nodes — callbacks stop firing and debugging gets painful fast.


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