<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abhinav Khedwal</title>
    <description>The latest articles on DEV Community by Abhinav Khedwal (@abhinav_khedwal_72181b607).</description>
    <link>https://dev.to/abhinav_khedwal_72181b607</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3958539%2Fee9a3a60-2208-4fe0-adc6-e634dfd75c9f.jpg</url>
      <title>DEV Community: Abhinav Khedwal</title>
      <link>https://dev.to/abhinav_khedwal_72181b607</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhinav_khedwal_72181b607"/>
    <language>en</language>
    <item>
      <title>My AI Chatbot's Latency Was 99.8% One Thing — and I Only Found Out Because I Traced It</title>
      <dc:creator>Abhinav Khedwal</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:44:53 +0000</pubDate>
      <link>https://dev.to/abhinav_khedwal_72181b607/my-ai-chatbots-latency-was-998-one-thing-and-i-only-found-out-because-i-traced-it-3k0o</link>
      <guid>https://dev.to/abhinav_khedwal_72181b607/my-ai-chatbots-latency-was-998-one-thing-and-i-only-found-out-because-i-traced-it-3k0o</guid>
      <description>&lt;p&gt;My AI Chatbot's Latency Was 99.8% One Thing — and I Only Found Out Because I Traced It&lt;/p&gt;

&lt;p&gt;I built EcoIQ, a carbon footprint tracker with a Groq-powered AI assistant, for a different hackathon a while back. It worked, it shipped, and the chat replies always felt a little slow — somewhere in the 1-1.5 second range. I assumed it was "just how AI apps are." I never actually knew where that time went.&lt;/p&gt;

&lt;p&gt;This weekend I wired the backend up to a self-hosted SigNoz instance to find out. The answer surprised me: 99.80% of a 1.45-second chat response was spent waiting on Groq's API. Not my validation logic, not my sanitization code, not JSON parsing — one outbound HTTPS call, sitting there, accounting for almost the entire trace.&lt;/p&gt;

&lt;p&gt;Here's the full story: the setup pain, the code, what the traces actually showed, and what I'd tell someone trying this for the first time.&lt;/p&gt;

&lt;p&gt;The problem, before I could see it&lt;/p&gt;

&lt;p&gt;EcoIQ's backend is a plain Node.js http server — no Express — with one route that matters for this story: POST /api/chat. It validates incoming messages, sanitizes them, checks a basic per-IP rate limit, and then proxies the request to Groq's chat completions endpoint:&lt;/p&gt;

&lt;p&gt;jsconst groqRes = await fetch('&lt;a href="https://api.groq.com/openai/v1/chat/completions" rel="noopener noreferrer"&gt;https://api.groq.com/openai/v1/chat/completions&lt;/a&gt;', {&lt;br&gt;
  method: 'POST',&lt;br&gt;
  headers: {&lt;br&gt;
    Authorization: &lt;code&gt;Bearer ${process.env.GROQ_API_KEY}&lt;/code&gt;,&lt;br&gt;
    'Content-Type': 'application/json',&lt;br&gt;
  },&lt;br&gt;
  body: JSON.stringify({&lt;br&gt;
    model: process.env.GROQ_MODEL || 'llama-3.3-70b-versatile',&lt;br&gt;
    messages: validation.sanitized,&lt;br&gt;
    temperature: 0.7,&lt;br&gt;
    max_tokens: 800,&lt;br&gt;
    stream: false,&lt;br&gt;
  }),&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;Before this weekend, if you'd asked me "why does this take 1.4 seconds," I'd have guessed. Maybe my validation loop was heavier than I thought. Maybe the rate-limiter's Map lookups added up. Maybe it was my own network. I had no actual evidence either way — just a route that felt slow and no way to break that feeling into numbers.&lt;/p&gt;

&lt;p&gt;Setting up SigNoz: further from docker compose up than I expected&lt;/p&gt;

&lt;p&gt;I went in expecting the classic install: clone the repo, docker compose up -d, done. That's not how it works anymore. SigNoz recently retired the bundled docker-compose files in favor of a new CLI called Foundry — I only found out because I hit a deprecation notice sitting in the repo's own deploy/README.md, after cd-ing into a folder that no longer had a compose file in it.&lt;/p&gt;

&lt;p&gt;The real friction came from Windows, though. Docker Desktop is the obvious default there, but SigNoz's own docs quietly warn that ClickHouse Keeper crashes under Docker Desktop's virtualization layer on Windows — segfaults (exit code 139) and restart loops. The reason, as best I can tell from their docs, comes down to how Docker Desktop's virtualization layer handles file system syncing between the Windows host and its utility VM — ClickHouse Keeper's disk access patterns don't play well with that translation layer. Their fix is to skip Docker Desktop entirely for this and run native Docker Engine inside WSL2 instead.&lt;/p&gt;

&lt;p&gt;So the real path looked like this:&lt;/p&gt;

&lt;p&gt;Confirm WSL2 is installed: wsl --version&lt;br&gt;
Install a real Ubuntu distro — not the hidden docker-desktop distro WSL drops you into by default, which has no package manager and no sudo:&lt;/p&gt;

&lt;p&gt;wsl --install -d Ubuntu&lt;/p&gt;

&lt;p&gt;Inside that Ubuntu shell, install Docker Engine natively:&lt;/p&gt;

&lt;p&gt;bash   curl -fsSL &lt;a href="https://get.docker.com" rel="noopener noreferrer"&gt;https://get.docker.com&lt;/a&gt; | sh&lt;br&gt;
   sudo usermod -aG docker $USER&lt;/p&gt;

&lt;p&gt;Install Foundry, SigNoz's new installer CLI:&lt;/p&gt;

&lt;p&gt;bash   curl -fsSL &lt;a href="https://signoz.io/foundry.sh" rel="noopener noreferrer"&gt;https://signoz.io/foundry.sh&lt;/a&gt; | bash&lt;/p&gt;

&lt;p&gt;Write a minimal casting.yaml:&lt;/p&gt;

&lt;p&gt;yaml   apiVersion: v1alpha1&lt;br&gt;
   kind: Installation&lt;br&gt;
   metadata:&lt;br&gt;
     name: signoz&lt;br&gt;
   spec:&lt;br&gt;
     deployment:&lt;br&gt;
       flavor: compose&lt;br&gt;
       mode: docker&lt;/p&gt;

&lt;p&gt;Deploy:&lt;/p&gt;

&lt;p&gt;bash   foundryctl cast -f casting.yaml&lt;/p&gt;

&lt;p&gt;That last command pulled five images — ClickHouse, ClickHouse Keeper, Postgres, the SigNoz backend, and the OTel collector — and started everything healthy on the first real attempt, but only after the WSL detour. If you're on Windows and want to avoid the crash loop entirely, start in WSL2 from minute one instead of finding out the hard way.&lt;/p&gt;

&lt;p&gt;A couple of minutes later, docker ps showed all five containers Up (healthy), and localhost:8080 gave me a live SigNoz workspace.&lt;/p&gt;

&lt;p&gt;Instrumenting the server without touching the route&lt;/p&gt;

&lt;p&gt;Adding OpenTelemetry took four packages and one small file — zero changes to server.cjs itself:&lt;/p&gt;

&lt;p&gt;bashnpm install --save-dev @opentelemetry/api @opentelemetry/sdk-node \&lt;br&gt;
  @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http&lt;/p&gt;

&lt;p&gt;js// tracing.cjs&lt;br&gt;
const { NodeSDK } = require('@opentelemetry/sdk-node');&lt;br&gt;
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');&lt;br&gt;
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');&lt;br&gt;
const { resourceFromAttributes } = require('@opentelemetry/resources');&lt;/p&gt;

&lt;p&gt;const sdk = new NodeSDK({&lt;br&gt;
  resource: resourceFromAttributes({ 'service.name': 'ecoiq-server' }),&lt;br&gt;
  traceExporter: new OTLPTraceExporter({ url: '&lt;a href="http://localhost:4318/v1/traces" rel="noopener noreferrer"&gt;http://localhost:4318/v1/traces&lt;/a&gt;' }),&lt;br&gt;
  instrumentations: [getNodeAutoInstrumentations()],&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;sdk.start();&lt;/p&gt;

&lt;p&gt;Then I preloaded it when starting the server instead of editing the entry point:&lt;/p&gt;

&lt;p&gt;bashnode -r ./tracing.cjs server.cjs&lt;/p&gt;

&lt;p&gt;That's the part that actually impressed me. getNodeAutoInstrumentations() recognizes Node's built-in http/https clients automatically — including the native fetch call inside handleChatAPI. I expected I'd need to hand-write a span around the Groq call to see anything about it. I didn't. It showed up on its own, tagged with the exact host, port, and full request URL.&lt;/p&gt;

&lt;p&gt;What the traces actually showed&lt;/p&gt;

&lt;p&gt;I opened the app, clicked through the calculator, and sent the AI assistant a message. Then I opened SigNoz's Traces Explorer.&lt;/p&gt;

&lt;p&gt;Show Image&lt;/p&gt;

&lt;p&gt;The list view alone told a story: plain page loads (GET requests for static HTML/CSS) landed in the 20-40ms range. Every POST to /api/chat landed between 1.2 and 1.45 seconds — a 30-40x gap that had nothing to do with my code and everything to do with the fact that one route calls out to an LLM and the other doesn't.&lt;/p&gt;

&lt;p&gt;Clicking into one of those POST traces (1.45s total) opened the waterfall view:&lt;/p&gt;

&lt;p&gt;Show Image&lt;/p&gt;

&lt;p&gt;The root span is POST /api/chat at 1.45s. Directly underneath it, stretching across almost the entire width of the flame graph, is a nested client span to api.groq.com — also 1.45s, marked as 99.80% of total execution time. My validation, sanitization, and rate-limit bookkeeping combined didn't register as a visible sliver on the graph. The span details panel showed exactly what got captured automatically: http.request.method: POST, http.response.status_code: 200, server.address: api.groq.com, server.port: 443, and the full url.full: &lt;a href="https://api.groq.com/openai/v1/chat/completions" rel="noopener noreferrer"&gt;https://api.groq.com/openai/v1/chat/completions&lt;/a&gt; — twelve attributes total, none of which I wrote a line of code for.&lt;/p&gt;

&lt;p&gt;I also checked the Service view (the auto-generated overview page SigNoz builds per service once it starts receiving data). Without any dashboard configuration on my part, it had already plotted the standard RED metrics — request rate, error rate, and duration percentiles — for ecoiq-server, splitting /api/chat out from the static-file routes by latency. Seeing the P99 duration line sitting close to 1.5s on that graph, next to a flat near-zero line for static assets, confirmed the same story from a different angle: this service has exactly one slow path, and it's not a mystery anymore.&lt;/p&gt;

&lt;p&gt;That's a genuinely useful thing to know if I ever want EcoIQ to feel faster. There's no point micro-optimizing validateMessages or the rate-limiter — the numbers say those cost microseconds, not milliseconds. The only lever that matters is the model call itself: a smaller/faster model, stream: true instead of waiting for the full completion before responding, or just an honest loading state that manages expectations instead of chasing a speedup that isn't there to find.&lt;/p&gt;

&lt;p&gt;What I'd tell past-me&lt;/p&gt;

&lt;p&gt;On Windows, start in WSL2, not Docker Desktop, for this stack specifically. The docs will tell you, but only if you read the fine print before you hit the crash yourself.&lt;br&gt;
Auto-instrumentation is not a toy. I assumed I'd need custom spans to see anything about an outbound fetch call. I didn't — the HTTP client instrumentation caught it automatically, full URL and all.&lt;br&gt;
A feeling ("this route is slow") is not a diagnosis. I'd had that feeling about /api/chat for weeks. It took one afternoon of tracing to turn it into an actual number I could act on, instead of a guess I kept deferring.&lt;br&gt;
Check the service overview, not just individual traces. The RED metrics view gave me the same conclusion as the waterfall, but as a pattern across many requests instead of one anecdote — worth doing both.&lt;/p&gt;

&lt;p&gt;Try it yourself&lt;/p&gt;

&lt;p&gt;If you're running any Node service that calls out to an LLM API and have a vague sense it's "slow," this setup takes under an hour end to end — most of that being the WSL detour if you're on Windows. The payoff is a concrete, attributed answer to "where does my latency actually go," instead of a guess you never quite get around to checking.&lt;/p&gt;

&lt;p&gt;SigNoz self-host Docker guide&lt;br&gt;
OpenTelemetry Node.js instrumentation&lt;br&gt;
EcoIQ live demo&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl1sbw5uaf8l98figvnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl1sbw5uaf8l98figvnq.png" alt=" " width="799" height="377"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch4x6i6ncn0bhjq7g6mp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch4x6i6ncn0bhjq7g6mp.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
