LangChain Hands-on · Module 4 · Lab 15

Callbacks, Caching, Rate Limits & LangSmith

⏱ 20 min + hands-on🎯 Intermediate🖼 1 diagram🧪 2 live demos🐍 Python code

Your app works in Colab. Now make it ready for real users: see what happens inside every call, stop paying twice for the same question, stay inside Groq’s free-tier limits, answer many users at once, and trace everything in LangSmith. These five extras are small code changes with a big effect on cost and reliability.

In this session

  • Callbacks: hooks on LLM start, end, error and new tokens
  • LLM cache: same question → no new API call
  • Rate limiter: stay inside requests-per-minute limits
  • Async gather and batch(max_concurrency)
  • LangSmith tracing with 3 environment variables
  • Production checklist

01The big idea: a dashboard, a memory and a speed limit

🚗

Think of a car on the Mumbai–Pune expressway. Callbacks are the dashboard: speed, fuel, warnings. The cache is remembering the route, so you don’t ask Google Maps again. The rate limiter is the speed limit, so you don’t get fined (HTTP 429). LangSmith is the dashcam: a full recording of every trip, for when something goes wrong.

Your appchain.invoke(q)Cache?hit → answer nowRate limiterwait for a slotGroq APIthe real callCallbacks + LangSmithlog time, tokens, trace
The path of one production call. A cache hit skips everything after step 2.
Demo · Try the LLM cache

Ask the same question twice with the cache on: the second answer is instant and free. Switch the cache off and the API is called every time. (Cost per call is an example value.)

02The five extras

ExtraCodeSaves you from
Callbackllm.invoke(q, config={"callbacks": [MyLogger()]})not knowing time and tokens per call
Cacheset_llm_cache(InMemoryCache()) or SQLiteCache("cache.db")paying twice for the same prompt
Rate limiterChatGroq(..., rate_limiter=InMemoryRateLimiter(requests_per_second=0.5))429 “too many requests” errors
Async / batchawait asyncio.gather(*[llm.ainvoke(q) …]), llm.batch(qs, config={"max_concurrency": 2})slow one-by-one loops
LangSmithLANGSMITH_TRACING=true + API key + projectdebugging blind
one by one (5 × 0.8 s)
4 s
batch, max_concurrency=2
2 s
async gather (all 5 at once)
1 s

Times for 5 questions from the hands-on run below (each fake call takes 0.8 s). Real Groq calls behave the same way.

Cache caution: only cache when the same prompt should give the same answer (FAQ, classification at temperature 0). Never cache personal answers like account balances across users.
langsmith.py
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = userdata.get("langsmith_key")     # free at smith.langchain.com
os.environ["LANGSMITH_PROJECT"] = "langchain-handson"

chain.invoke({"topic": "vector database"},
             config={"run_name": "explain-topic", "tags": ["demo"], "metadata": {"user": "ajeet"}})
# open smith.langchain.com → every step with inputs, outputs, tokens, latency and errors

03Hands-on: run it here

Callbacks, token streaming, cache, rate limiter and concurrency, timed for real. The stand-in waits 0.8 s per call, like a network round trip, so you can see the cache and concurrency effects.

lab15_production.py
import time, asyncio
from standins import RuleModel
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
from langchain_core.rate_limiters import InMemoryRateLimiter

# a stand-in with 0.8 s of fake network delay, so timings look like a real API
llm = RuleModel(default="India has 22 official languages listed in the Constitution.", delay=0.8)

class MyLogger(BaseCallbackHandler):
    def on_chat_model_start(self, serialized, messages, **kwargs):
        self.start = time.time(); print("🚀 LLM started")
    def on_llm_end(self, response, **kwargs):
        usage = response.generations[0][0].message.usage_metadata
        print(f"✅ LLM finished in {time.time() - self.start:.1f}s | tokens: {usage}")
llm.invoke("Tell me a fun fact about India", config={"callbacks": [MyLogger()]})

class PrintTokens(BaseCallbackHandler):
    def on_llm_new_token(self, token, **kwargs): print(token, end="|")
print("tokens: ", end="")
for _ in RuleModel(default="Snow peaks touch the sky.").stream("Himalayas", config={"callbacks": [PrintTokens()]}): pass
print()

set_llm_cache(InMemoryCache())
for i in (1, 2):
    t = time.time(); llm.invoke("What is the capital of Japan?")
    print(f"call {i}: {time.time() - t:.1f} s" + ("  (from cache, no API cost)" if i == 2 else ""))
set_llm_cache(None)

limited = RuleModel(default="hi", rate_limiter=InMemoryRateLimiter(requests_per_second=0.5, check_every_n_seconds=0.1, max_bucket_size=1))
start = time.time()
for i in range(3):
    limited.invoke(f"hi {i}"); print(f"rate-limited call {i + 1} finished at {time.time() - start:.0f} s")

questions = [f"Give one fact about number {i}" for i in range(1, 6)]
t = time.time(); [llm.invoke(q) for q in questions]; seq = time.time() - t
async def main(): return await asyncio.gather(*[llm.ainvoke(q) for q in questions])
t = time.time(); asyncio.run(main()); par = time.time() - t
t = time.time(); llm.batch(questions, config={"max_concurrency": 2}); b2 = time.time() - t
print(f"5 questions: one by one {seq:.0f} s | async gather {par:.0f} s | batch(max_concurrency=2) {b2:.0f} s")
🚀 LLM started ✅ LLM finished in 0.8s | tokens: {'input_tokens': 7, 'output_tokens': 9, 'total_tokens': 16} tokens: Snow |peaks |touch |the |sky.|| call 1: 0.8 s call 2: 0.0 s (from cache, no API cost) rate-limited call 1 finished at 2 s rate-limited call 2 finished at 4 s rate-limited call 3 finished at 6 s 5 questions: one by one 4 s | async gather 1 s | batch(max_concurrency=2) 2 s
Runs here without any API key. A small stand-in plays the LLM (RuleModel or ScriptedModel from standins.py), so every output below is from a real run of this code. In Colab you swap one line: llm = ChatGroq(model="openai/gpt-oss-20b").

04Hands-on: Ajeetkumar’s Colab notebook

Colab setup (same in every notebook): click 🔑 Secrets in the Colab sidebar, add groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.
Demo · Notebook player · Lab 15

05Production checklist

✅How
API keys in secrets, never in codeColab Secrets, .env, a cloud secret manager
timeout, max_retries, fallbacksChatGroq(timeout=30, max_retries=2), .with_fallbacks()
cache repeated calls, rate-limit free tiersthis lab
trace with LangSmith, log tokens and costthis lab
test prompts on a fixed question set before changing themLangSmith datasets and evaluations

06Summary

07Quick quiz

1. Same question asked twice with InMemoryCache on. Second call?

2. requests_per_second=0.5 means…

3. Which callback method prints streamed tokens?

4. How do you switch on LangSmith tracing?

5. When should you NOT cache?

08References

Created by Ajeetkumar