Course Content
LangChain Mastery
7 sections · 109 lessons
Implement a LangChain callback for logging execution time.
What you need to know
Python
1import time, logging2from langchain_core.callbacks import BaseCallbackHandler34class LatencyLogger(BaseCallbackHandler):5 def __init__(self):6 self.starts = {}78 def on_chain_start(self, serialized, inputs, *, run_id, **kw):9 self.starts[run_id] = (time.perf_counter(), kw.get("name", "chain"))1011 def on_chat_model_start(self, serialized, messages, *, run_id, **kw):12 name = kw.get("name") or (serialized or {}).get("name", "model")13 self.starts[run_id] = (time.perf_counter(), name)1415 def on_chain_end(self, outputs, *, run_id, **kw):16 self._done(run_id, "ok")1718 def on_llm_end(self, response, *, run_id, **kw):19 self._done(run_id, "ok")2021 def on_chain_error(self, error, *, run_id, **kw):22 self._done(run_id, "error")2324 def on_llm_error(self, error, *, run_id, **kw):25 self._done(run_id, "error")2627 def _done(self, run_id, status):28 t0, name = self.starts.pop(run_id, (None, None))29 if t0 is not None:30 ms = (time.perf_counter() - t0) * 100031 logging.info("%s %s %.0f ms", name, status, ms)3233chain.invoke(inputs, config={"callbacks": [LatencyLogger()]})Running this on prompt | llm | parser logs one line per step and one for the whole sequence.
Details that matter
run_idkeys: abatchcall runs many steps at once, so a single "start time" variable would be overwritten. The dict keyed byrun_idkeeps them apart.- Chat models fire
on_chat_model_start, noton_llm_start, but both finish withon_llm_end. - Error hooks: without
on_chain_error, failed runs leave entries inself.startsforever, a slow memory leak. - Where to attach:
config={"callbacks": [...]}applies to that call and every step inside it. Passingcallbacks=to a model's constructor only covers that model. - Async code: use
AsyncCallbackHandlerif your handler does I/O, so it does not block the event loop. - Names:
.with_config(run_name="retrieve_docs")gives a step a readable name in logs and traces.
A real-life example
A support bot over a telecom company's help-centre docs had a p95 latency of 7 seconds, and nobody knew why. The team added a latency handler that sent each step's duration to Prometheus, labelled by run_name. In one day the graphs showed the model took 1.8 seconds at p95, but a "rerank" step took 4.1 seconds because it called an external API one document at a time. Batching the rerank calls brought p95 down to 3.2 seconds. The handler stayed in production as a cheap metric feed, while LangSmith was used for looking at individual slow traces.
Follow-up questions to expect
- "Why not just wrap
invokewith a timer?" — That gives only the total. The callback gives per-step timings inside the chain, which is where the answer usually is. - "How would you log token usage too?" — In
on_llm_end, readresponse.generations[0][0].message.usage_metadataand log the counts with the samerun_id. - "LangSmith or a custom handler?" — LangSmith for debugging and trace inspection; a custom handler to send metrics to your own monitoring. Many teams use both.