<?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: Preetham</title>
    <description>The latest articles on DEV Community by Preetham (@preetham_25a78ec384dac787).</description>
    <link>https://dev.to/preetham_25a78ec384dac787</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%2F3828262%2Fe0ea9c10-7e96-410e-88e3-26229545c0e0.png</url>
      <title>DEV Community: Preetham</title>
      <link>https://dev.to/preetham_25a78ec384dac787</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/preetham_25a78ec384dac787"/>
    <language>en</language>
    <item>
      <title>The 300 milliseconds that decide whether your AI sounds human</title>
      <dc:creator>Preetham</dc:creator>
      <pubDate>Sun, 26 Jul 2026 17:55:38 +0000</pubDate>
      <link>https://dev.to/preetham_25a78ec384dac787/the-300-milliseconds-that-decide-whether-your-ai-sounds-human-4i63</link>
      <guid>https://dev.to/preetham_25a78ec384dac787/the-300-milliseconds-that-decide-whether-your-ai-sounds-human-4i63</guid>
      <description>&lt;p&gt;Two people having a conversation leave about &lt;strong&gt;200 milliseconds&lt;/strong&gt; between&lt;br&gt;
turns. That gap is remarkably consistent — across languages, across cultures,&lt;br&gt;
across phone lines. It is roughly a fifth of a second, and we are exquisitely&lt;br&gt;
sensitive to it.&lt;/p&gt;

&lt;p&gt;Push it to half a second and the other person sounds hesitant. Push it to a&lt;br&gt;
full second and they sound distracted, or like they did not hear you. You will&lt;br&gt;
start to repeat yourself.&lt;/p&gt;

&lt;p&gt;I spent last week measuring that gap on an AI voice agent. The number came back&lt;br&gt;
at &lt;strong&gt;1,107 milliseconds&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "it feels laggy" is such a useless bug report
&lt;/h2&gt;

&lt;p&gt;I have written that bug report. You probably have too. It is genuinely all you&lt;br&gt;
can say, because the thing that went wrong is a &lt;em&gt;feeling&lt;/em&gt; — and feelings do not&lt;br&gt;
show up in dashboards.&lt;/p&gt;

&lt;p&gt;Here is what makes it hard. When someone says a voice assistant feels slow,&lt;br&gt;
there are at least four completely different things they might mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It took a long time to &lt;strong&gt;start&lt;/strong&gt; answering.&lt;/li&gt;
&lt;li&gt;It answered quickly but &lt;strong&gt;rambled&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It kept &lt;strong&gt;stopping mid-sentence&lt;/strong&gt; and starting again.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;talked over&lt;/strong&gt; them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are four separate faults with four separate fixes, and they feel almost&lt;br&gt;
identical to the person on the phone. Worse, in the systems we normally use to&lt;br&gt;
watch software, they look identical too — the same numbers, none of them wrong,&lt;br&gt;
none of them useful.&lt;/p&gt;

&lt;p&gt;So I built something to tell them apart.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we were measuring, and what we were missing
&lt;/h2&gt;

&lt;p&gt;Almost every tool for watching AI systems assumes a shape: you ask a question,&lt;br&gt;
you get an answer, and the tool measures the time in between. That is a&lt;br&gt;
perfectly good model for a chatbot.&lt;/p&gt;

&lt;p&gt;Voice does not work like that. Both people are on the line the whole time. You&lt;br&gt;
can interrupt. It can interrupt you. There is no clean moment where your turn&lt;br&gt;
ends and its turn begins — that boundary is something you both &lt;em&gt;negotiate&lt;/em&gt;,&lt;br&gt;
constantly, without thinking about it.&lt;/p&gt;

&lt;p&gt;Which means the most important measurement — how long you sat in silence&lt;br&gt;
waiting — is not a thing the existing tools even have a name for. It falls in&lt;br&gt;
the gap &lt;em&gt;between&lt;/em&gt; the things they measure.&lt;/p&gt;

&lt;p&gt;So I drew it.&lt;/p&gt;

&lt;p&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%2Fp3lbrjzubiu0vp3plqds.jpg" 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%2Fp3lbrjzubiu0vp3plqds.jpg" alt="The silence between speakers, drawn as an amber gap" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blue is the person talking. Violet is the agent. The amber band in the middle&lt;br&gt;
is the silence, with the milliseconds counting up while it lasts.&lt;/p&gt;

&lt;p&gt;I did not expect this to be the useful part. It was meant to be a debugging&lt;br&gt;
view. But watching that number climb while you sit there waiting communicates&lt;br&gt;
something a percentile chart cannot: you &lt;em&gt;feel&lt;/em&gt; the delay rather than reading&lt;br&gt;
about it. Two seconds of silence is unbearable on screen in a way that "2000ms"&lt;br&gt;
simply is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interruption is the interesting signal
&lt;/h2&gt;

&lt;p&gt;The measurement I did not anticipate finding useful was &lt;strong&gt;where&lt;/strong&gt; people&lt;br&gt;
interrupt.&lt;/p&gt;

&lt;p&gt;Not &lt;em&gt;how often&lt;/em&gt; — that number turns out to be nearly meaningless on its own.&lt;br&gt;
Two agents can have identical interruption rates and completely opposite&lt;br&gt;
problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If people cut in &lt;strong&gt;almost immediately&lt;/strong&gt; — within a few hundred milliseconds —
they are usually not interrupting at all. The agent is mistaking background
noise for speech and stopping itself. A door closing. A cough. Someone else
in the room.&lt;/li&gt;
&lt;li&gt;If people cut in &lt;strong&gt;several seconds&lt;/strong&gt; into the answer, they are genuinely
interrupting, and it means the agent is talking too much. They got what they
needed in the first sentence and are trying to move on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same count. Opposite causes. Opposite fixes. You only see the difference if you&lt;br&gt;
record &lt;em&gt;when&lt;/em&gt; inside the reply the interruption landed, and almost nothing&lt;br&gt;
records that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "good" should actually mean
&lt;/h2&gt;

&lt;p&gt;Numbers are only useful once you decide which ones are acceptable. So I wrote&lt;br&gt;
down six, with a reason for each rather than a round number that felt nice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Silence before replying&lt;/td&gt;
&lt;td&gt;under 350 ms&lt;/td&gt;
&lt;td&gt;Past this it reads as hesitation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interruptions per call&lt;/td&gt;
&lt;td&gt;under 0.8&lt;/td&gt;
&lt;td&gt;Above this, people are fighting to be heard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Talking over the user&lt;/td&gt;
&lt;td&gt;under 150 ms&lt;/td&gt;
&lt;td&gt;Beyond this it feels rude, not eager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calls resolved without a human&lt;/td&gt;
&lt;td&gt;above 72%&lt;/td&gt;
&lt;td&gt;The number the deployment is funded on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turns where the user repeated themselves&lt;/td&gt;
&lt;td&gt;under 9%&lt;/td&gt;
&lt;td&gt;The closest thing to "did it work"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalations to a human&lt;/td&gt;
&lt;td&gt;under 11%&lt;/td&gt;
&lt;td&gt;The commercial ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&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%2Fm66awh5xdnpjgy2y26tf.jpg" 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%2Fm66awh5xdnpjgy2y26tf.jpg" alt="The six objectives, with targets drawn on each panel" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fifth one is my favourite, and it took the longest to arrive at. &lt;strong&gt;How&lt;br&gt;
often did the person have to say it again?&lt;/strong&gt; Not the latency, not the token&lt;br&gt;
count — whether the conversation actually worked. When someone says &lt;em&gt;"no, I&lt;br&gt;
meant the other one"&lt;/em&gt; or &lt;em&gt;"can you repeat that"&lt;/em&gt;, something failed a turn&lt;br&gt;
earlier. It is the closest thing to measuring understanding that I could find&lt;br&gt;
without asking anybody to fill in a survey.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part where I was wrong
&lt;/h2&gt;

&lt;p&gt;I built all of this against a simulator, because you cannot design dashboards&lt;br&gt;
against three hand-held test calls. I tuned that simulator to what I believed&lt;br&gt;
good performance looked like: replies starting in about 250 milliseconds.&lt;/p&gt;

&lt;p&gt;Then I pointed the whole thing at a real Gemini Live agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1,107 milliseconds.&lt;/strong&gt; Three to four times slower than the world I had spent a&lt;br&gt;
day designing for.&lt;/p&gt;

&lt;p&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%2Fygd9cpbzv5ed88jnm30t.jpg" 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%2Fygd9cpbzv5ed88jnm30t.jpg" alt="Real sessions plotted against the simulated baseline" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The orange line is reality. The two flat lines below it are what I had assumed.&lt;/p&gt;

&lt;p&gt;I could have quietly adjusted the simulator so the numbers agreed. I left it,&lt;br&gt;
because that gap &lt;em&gt;is&lt;/em&gt; the finding — and because an instrument that only ever&lt;br&gt;
confirms what you already believed is not an instrument, it is decoration.&lt;/p&gt;

&lt;p&gt;It also caught two genuine bugs in my own measuring code that thirty-five&lt;br&gt;
automated tests had missed. Which is its own small lesson: the tests checked&lt;br&gt;
that the code did what I told it to. Only real data checked whether what I told&lt;br&gt;
it was right.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you are building one of these
&lt;/h2&gt;

&lt;p&gt;Three things I would tell myself a week ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure the silence, not the duration.&lt;/strong&gt; How long the whole exchange took is&lt;br&gt;
almost irrelevant. How long the person waited before hearing &lt;em&gt;anything&lt;/em&gt; is&lt;br&gt;
nearly everything. They are different numbers and only one of them predicts&lt;br&gt;
whether people enjoy talking to your agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record where interruptions happen, not just that they did.&lt;/strong&gt; A count tells&lt;br&gt;
you something is wrong. The distribution tells you what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Point it at reality earlier than feels comfortable.&lt;/strong&gt; I lost a day to&lt;br&gt;
assumptions that a single real call would have corrected. The simulator was&lt;br&gt;
useful — it just was not true.&lt;/p&gt;




&lt;p&gt;None of this is really about AI. It is about the fact that conversation has&lt;br&gt;
rules we all follow without noticing, and we notice instantly when a machine&lt;br&gt;
breaks them. The 200 millisecond gap. Yielding when someone cuts in. Not&lt;br&gt;
repeating what was already understood.&lt;/p&gt;

&lt;p&gt;We have got very good at making these systems say the right words. Getting the&lt;br&gt;
timing right is a separate problem, and right now most of us are not even&lt;br&gt;
measuring it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tooling is open source, along with a short demo:&lt;br&gt;
&lt;a href="https://github.com/preethamresearch/cadence" rel="noopener noreferrer"&gt;github.com/preethamresearch/cadence&lt;/a&gt;&lt;br&gt;
· &lt;a href="https://youtu.be/fIbf-5_2tRQ" rel="noopener noreferrer"&gt;watch it&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for the Agents of SigNoz hackathon. Written by me; I used an AI&lt;br&gt;
assistant for the code and for editing this post, which is declared in the&lt;br&gt;
submission.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ux</category>
      <category>webdev</category>
      <category>observability</category>
    </item>
    <item>
      <title>SigNoz scanned 36,000 rows and told me "No Data"</title>
      <dc:creator>Preetham</dc:creator>
      <pubDate>Sun, 26 Jul 2026 17:53:37 +0000</pubDate>
      <link>https://dev.to/preetham_25a78ec384dac787/signoz-scanned-36000-rows-and-told-me-no-data-1o39</link>
      <guid>https://dev.to/preetham_25a78ec384dac787/signoz-scanned-36000-rows-and-told-me-no-data-1o39</guid>
      <description>&lt;p&gt;I spent an hour convinced my instrumentation was broken. It wasn't. The&lt;br&gt;
telemetry was arriving, ClickHouse was reading it, and every dashboard panel&lt;br&gt;
still said &lt;strong&gt;No Data&lt;/strong&gt; — over a table that had 36,291 rows in it.&lt;/p&gt;

&lt;p&gt;That was one of eight bugs I hit last week building OpenTelemetry&lt;br&gt;
instrumentation for real-time voice agents. Not one of them threw an&lt;br&gt;
exception. Not one showed up in a log. Six of them I found &lt;em&gt;after&lt;/em&gt; the code&lt;br&gt;
was already "working."&lt;/p&gt;

&lt;p&gt;This post is the list, with the commands I used to find each one. If you are&lt;br&gt;
instrumenting anything that streams — voice, video, an agent loop — you will&lt;br&gt;
probably hit at least three of these.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I was building, briefly
&lt;/h2&gt;

&lt;p&gt;Voice agents like Gemini Live hold a &lt;strong&gt;persistent bidirectional socket&lt;/strong&gt;.&lt;br&gt;
Audio goes both ways at once. There is no request and no response, so the&lt;br&gt;
OpenTelemetry GenAI conventions — which define &lt;code&gt;chat&lt;/code&gt; spans around a&lt;br&gt;
request/response pair — have nothing to attach to.&lt;/p&gt;

&lt;p&gt;The number that actually matters for a voice agent is &lt;strong&gt;time to first audio&lt;/strong&gt;:&lt;br&gt;
how long the human sat in silence after they stopped talking. That interval&lt;br&gt;
lives &lt;em&gt;between&lt;/em&gt; spans, so no waterfall draws it.&lt;/p&gt;

&lt;p&gt;So I built &lt;code&gt;cadence&lt;/code&gt;: a state machine that reconstructs turn structure from the&lt;br&gt;
raw signal stream and emits it as ordinary OTLP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;realtime.session                        one connected session
├── realtime.turn                       one exchange
│   ├── realtime.audio.user_utterance   VAD start → end
│   ├── chat                            [standard gen_ai.* attributes]
│   │   └── execute_tool
│   └── realtime.audio.agent_utterance  first audio out → done
│       └── (event) realtime.barge_in   offset_ms into the reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&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%2Fvpbkfqhtkxccmajnk2g1.jpg" 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%2Fvpbkfqhtkxccmajnk2g1.jpg" alt="Real Gemini Live session as an OpenTelemetry trace in SigNoz"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a real Gemini session in SigNoz. Getting there took the eight bugs&lt;br&gt;
below.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. The collector was running, and doing nothing
&lt;/h2&gt;

&lt;p&gt;I installed SigNoz with Foundry, which brings up the backend &lt;em&gt;and&lt;/em&gt; its MCP&lt;br&gt;
server in one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; deploy/casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers healthy. Port 4318 open. And the exporter kept logging&lt;br&gt;
&lt;code&gt;RemoteDisconnected&lt;/code&gt;. &lt;code&gt;curl&lt;/code&gt; confirmed it — TCP connected, then the server hung&lt;br&gt;
up without an HTTP response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;* Connected to localhost (127.0.0.1) port 4318
&amp;gt; &lt;/span&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/v1/traces&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="s"&gt;* Empty reply from server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The effective collector config explains it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;signoz-ingester-1 sh &lt;span class="nt"&gt;-lc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"awk '/^service:/{f=1} f' /var/tmp/collector-config.yaml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;nop&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;nop&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every pipeline was &lt;code&gt;nop&lt;/code&gt;. The config &lt;em&gt;file&lt;/em&gt; on disk was correct — the running&lt;br&gt;
collector had never received it. The reason was in the SigNoz server log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to find or create agent ... "cannot create agent without orgId"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SigNoz will not hand a collector its configuration until an organisation&lt;br&gt;
exists.&lt;/strong&gt; I had not completed the first-run signup at &lt;code&gt;localhost:8080&lt;/code&gt;. Until&lt;br&gt;
you do, the OTLP port accepts connections and answers nothing.&lt;/p&gt;

&lt;p&gt;Do the signup, restart the ingester, and the pipelines populate.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The exporter silently dropped 60% of my spans
&lt;/h2&gt;

&lt;p&gt;I generated 1,201 turns. ClickHouse had 497.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error surfaced. The OpenTelemetry &lt;code&gt;BatchSpanProcessor&lt;/code&gt; defaults to a&lt;br&gt;
&lt;strong&gt;2,048-span queue&lt;/strong&gt;, and a realtime workload overruns it instantly — each turn&lt;br&gt;
produces four to six spans, so a few hundred concurrent turns is enough. When&lt;br&gt;
the queue is full, spans are dropped, and the SDK does not raise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v1/traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;max_queue_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32_768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# default 2048 — far too small for streaming
&lt;/span&gt;    &lt;span class="n"&gt;max_export_batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schedule_delay_millis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-ran it: 1,201 turns generated, 1,201 delivered.&lt;/p&gt;




&lt;h2&gt;
  
  
  3, 4, 5. Three ways to get timestamps wrong
&lt;/h2&gt;

&lt;p&gt;I replay recorded sessions to populate dashboards, and replayed spans came out&lt;br&gt;
&lt;strong&gt;0.06 ms long&lt;/strong&gt; while their attributes claimed 400 ms. Spans were being stamped&lt;br&gt;
at wall-clock "now" rather than at the time the event carried, so a session&lt;br&gt;
replayed in one second produced microsecond spans.&lt;/p&gt;

&lt;p&gt;The OpenTelemetry API accepts explicit timestamps; use them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wall_ns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wall_ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then every span landed near &lt;strong&gt;1970&lt;/strong&gt; — because I passed &lt;code&gt;time.monotonic()&lt;/code&gt; as&lt;br&gt;
the wall clock. Monotonic is time since boot, not since epoch.&lt;/p&gt;

&lt;p&gt;And the session span came out with a duration of several hours, because I&lt;br&gt;
opened it eagerly at "now" and closed it in the replayed past. Negative&lt;br&gt;
duration, wrapped unsigned.&lt;/p&gt;

&lt;p&gt;The one worth stealing: &lt;strong&gt;compare two views of the same quantity.&lt;/strong&gt; The span&lt;br&gt;
duration and the &lt;code&gt;duration_ms&lt;/code&gt; attribute should agree, so check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attributes_string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'realtime.prompt.version'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;durationNano&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;span_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes_number&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'realtime.turn.duration_ms'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;attr_ms&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'realtime.turn'&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v16    4863    26135501
v17    5344    26095317
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 4.8-second span with a &lt;code&gt;duration_ms&lt;/code&gt; attribute of &lt;strong&gt;26,135,501&lt;/strong&gt; — seven&lt;br&gt;
hours. Durations were computed from &lt;code&gt;time.monotonic()&lt;/code&gt; while spans were stamped&lt;br&gt;
from the event clock. In a live session the two coincide and this is invisible.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Cumulative vs delta: the "No Data" over 36,000 rows
&lt;/h2&gt;

&lt;p&gt;This is the one from the title.&lt;/p&gt;

&lt;p&gt;Metrics were arriving. &lt;code&gt;signoz_metrics.distributed_samples_v4&lt;/code&gt; had them. Every&lt;br&gt;
percentile and rate query returned nothing, and the response was quietly&lt;br&gt;
explicit about how much work it had done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rowsScanned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;36291&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bytesScanned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;495851&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"queryName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aggregations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scanned 36,291 rows. Aggregated to &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SigNoz expects delta temporality for counters and histograms. The&lt;br&gt;
OpenTelemetry Python SDK defaults to cumulative.&lt;/strong&gt; Nothing errors — the data is&lt;br&gt;
stored, and every rate and percentile computes to nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Histogram&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.metrics.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AggregationTemporality&lt;/span&gt;

&lt;span class="nc"&gt;OTLPMetricExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v1/metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;preferred_temporality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AggregationTemporality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DELTA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AggregationTemporality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DELTA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One dictionary. Every panel populated.&lt;/p&gt;

&lt;p&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%2Fm66awh5xdnpjgy2y26tf.jpg" 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%2Fm66awh5xdnpjgy2y26tf.jpg" alt="Conversation SLO dashboard with target lines drawn"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth checking with the MCP server, which auto-fetches the metric's metadata&lt;br&gt;
and tells you what it thinks it is holding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metricType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;histogram (auto-fetched)&lt;/span&gt;
&lt;span class="na"&gt;temporality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;delta (auto-fetched)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7 and 8. The two bugs only the real API found
&lt;/h2&gt;

&lt;p&gt;Everything above was found against a simulator. So I pointed it at the actual&lt;br&gt;
Gemini Live API — and my adapter, the one component that touches the vendor,&lt;br&gt;
had never processed a real message.&lt;/p&gt;

&lt;p&gt;It found two more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Barge-in counted against finished turns.&lt;/strong&gt; Real sessions reported 22 barge-in&lt;br&gt;
events and &lt;strong&gt;0% interrupted turns&lt;/strong&gt;. Those numbers must agree. The flag marking&lt;br&gt;
"the agent is speaking" was only cleared by a playback event my application&lt;br&gt;
never emitted, so every turn after the first looked like an interruption — and&lt;br&gt;
the barge-in was attributed to a span that had been exported seconds earlier.&lt;/p&gt;

&lt;p&gt;The subtlety in the fix: clear that flag when a turn &lt;em&gt;completes&lt;/em&gt;, but &lt;strong&gt;not&lt;/strong&gt;&lt;br&gt;
when it is interrupted. On an interruption the turn ends the instant the user&lt;br&gt;
cuts in while the agent's audio keeps playing — and that lingering audio is&lt;br&gt;
exactly the overlap being measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the latency was nothing like I assumed.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Turns&lt;/th&gt;
&lt;th&gt;Mean TTFA&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;simulated &lt;code&gt;v16&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;416&lt;/td&gt;
&lt;td&gt;246 ms&lt;/td&gt;
&lt;td&gt;436 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;simulated &lt;code&gt;v17&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;328 ms&lt;/td&gt;
&lt;td&gt;716 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;real Gemini Live&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,107 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,658 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&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%2Fygd9cpbzv5ed88jnm30t.jpg" 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%2Fygd9cpbzv5ed88jnm30t.jpg" alt="TTFA split by prompt version, with the real series well above both"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Real Gemini Live is three to four times slower than the baseline I had spent a&lt;br&gt;
day designing against. My 350 ms objective comes from turn-taking research and&lt;br&gt;
I still think it is right for a &lt;em&gt;spoken&lt;/em&gt; turn — but it is nowhere near&lt;br&gt;
achievable on text-driven turns through this model today.&lt;/p&gt;

&lt;p&gt;I left that in the README as found rather than quietly retuning the simulator&lt;br&gt;
to match. Instruments that agree with your assumptions are not instruments.&lt;/p&gt;


&lt;h2&gt;
  
  
  The thing I actually took away
&lt;/h2&gt;

&lt;p&gt;Every one of these eight failures was &lt;strong&gt;silent&lt;/strong&gt;. The application reported&lt;br&gt;
success throughout. That is the property you want in production — your voice&lt;br&gt;
agent must not die because telemetry is unhappy — and it is precisely what&lt;br&gt;
makes this miserable to debug.&lt;/p&gt;

&lt;p&gt;So I wrote a preflight check that walks the chain and names the broken link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;library     ✓ cadence imports            v0.1.0, schema 0.1.0
            ✓ turn state machine         TTFA 400ms (expected 400ms)
signoz      ✓ SigNoz set up              v0.134.0
            ✓ collector pipelines        real receivers and exporters
transport   ✓ OTLP port reachable
            ✓ OTLP endpoint answers      HTTP 200
round trip  ✓ span exported end-to-end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in there matter more than the rest. It checks &lt;strong&gt;TCP reachability&lt;br&gt;
separately from "does it speak HTTP"&lt;/strong&gt;, because a &lt;code&gt;nop&lt;/code&gt; pipeline accepts the&lt;br&gt;
connection and closes it — that distinction is bug #1 and it cost me an hour.&lt;br&gt;
And it drives a synthetic turn through the real state machine and asserts TTFA&lt;br&gt;
is &lt;strong&gt;400 ms, not 2,400 ms&lt;/strong&gt;, because silently wrong telemetry is worse than an&lt;br&gt;
outage: nothing breaks, you just make decisions on bad numbers.&lt;/p&gt;

&lt;p&gt;If you take one thing from this: &lt;strong&gt;compare two views of the same quantity.&lt;/strong&gt;&lt;br&gt;
Span duration against the duration attribute. Barge-in events against&lt;br&gt;
interrupted turns. That single habit found three of my eight bugs, and none of&lt;br&gt;
them would have surfaced any other way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code, the `realtime.&lt;/em&gt;` semantic conventions, and importable SigNoz dashboards:&lt;br&gt;
&lt;a href="https://github.com/preethamresearch/cadence" rel="noopener noreferrer"&gt;github.com/preethamresearch/cadence&lt;/a&gt;&lt;br&gt;
· &lt;a href="https://youtu.be/fIbf-5_2tRQ" rel="noopener noreferrer"&gt;demo&lt;/a&gt; · &lt;a href="https://youtu.be/CZ2TeXH-yFY" rel="noopener noreferrer"&gt;full walkthrough&lt;/a&gt;*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for the Agents of SigNoz hackathon. Written by me; I used an AI&lt;br&gt;
assistant for the code and for editing this post, which is declared in the&lt;br&gt;
submission.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>debugging</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building G-Axis: A Voice AI Companion + Browser Agent with Gemini Live API</title>
      <dc:creator>Preetham</dc:creator>
      <pubDate>Mon, 16 Mar 2026 23:57:26 +0000</pubDate>
      <link>https://dev.to/preetham_25a78ec384dac787/building-g-axis-a-voice-ai-companion-browser-agent-with-gemini-live-api-l4k</link>
      <guid>https://dev.to/preetham_25a78ec384dac787/building-g-axis-a-voice-ai-companion-browser-agent-with-gemini-live-api-l4k</guid>
      <description>&lt;p&gt;Your browser already works. You don't need a new app to experience AI.&lt;/p&gt;

&lt;p&gt;That was the idea behind &lt;strong&gt;G-Axis&lt;/strong&gt; — a Chrome extension I built for the Gemini Live Agent Challenge that turns your&lt;br&gt;
  existing browser into an AI-powered workspace.&lt;/p&gt;

&lt;p&gt;No new tabs. No new logins. Just intelligence, right where you already work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Ever had 6 tabs open just to do one thing? ChatGPT here, Calendar there, Google Search somewhere else. Every AI tool&lt;br&gt;
  lives in its own silo. And they can all &lt;em&gt;talk&lt;/em&gt; — but none of them can actually &lt;em&gt;do&lt;/em&gt; anything in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What G-Axis Does
&lt;/h2&gt;

&lt;p&gt;Two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Talk to it&lt;/strong&gt; — Click the mic, pick one of 8 AI personas, and have a real conversation. Not text-to-speech. Real&lt;br&gt;
  bidirectional audio via Gemini's Live API. Ask it anything — it searches the web in real-time via Google Search&lt;br&gt;
  grounding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delegate to it&lt;/strong&gt; — Type "Plan a 5-day Japan itinerary" and watch it research, navigate, and generate a full&lt;br&gt;
  document. Type "Schedule a meeting tomorrow at 10am" and it opens Calendar, fills the form, and saves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gemini Stack
&lt;/h2&gt;

&lt;p&gt;Here's what powers it under the hood:&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini Live API — The Voice Engine
&lt;/h3&gt;

&lt;p&gt;This was the breakthrough. Gemini's native audio model (&lt;code&gt;gemini-2.5-flash-native-audio&lt;/code&gt;) handles real-time voice&lt;br&gt;
  natively — no separate STT/TTS pipeline. The extension's service worker connects directly via WebSocket. Zero hops.&lt;br&gt;
  Zero latency.&lt;/p&gt;

&lt;p&gt;I built 8 personas on top of it, each with a different Gemini voice and personality:&lt;/p&gt;

&lt;p&gt;| Persona | Voice | Vibe |&lt;br&gt;
|---------|-------|------|&lt;br&gt;
| Friendly Buddy | Puck | Your go-to friend |&lt;br&gt;
| Wise Mentor | Charon | Guidance, not lectures |&lt;br&gt;
| Creative Partner | Aoede | Ideas machine |&lt;br&gt;
| Job Interviewer | Kore | Practice makes perfect |&lt;br&gt;
| Chill Companion | Fenrir | Just vibes |&lt;br&gt;
| Professional Coach | Kore | Sharpen your edge |&lt;br&gt;
| Friendly Debater | Charon | Challenge your thinking |&lt;br&gt;
| Storyteller | Aoede | Bring ideas to life |&lt;/p&gt;

&lt;p&gt;Switch mid-conversation. The voice changes. The personality changes. The previous session saves automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Search — Real-Time Grounding
&lt;/h3&gt;

&lt;p&gt;Ask "What's the latest AI news?" and Gemini doesn't guess from training data — it searches the web live and answers&lt;br&gt;
  with current information. This is the &lt;code&gt;google_search&lt;/code&gt; tool baked into the Live API config.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini 2.5 Flash — The Brain
&lt;/h3&gt;

&lt;p&gt;Task planning. Function calling. Session analysis. Every voice conversation gets analyzed for 5 communication skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confidence&lt;/li&gt;
&lt;li&gt;Clarity&lt;/li&gt;
&lt;li&gt;Engagement&lt;/li&gt;
&lt;li&gt;Listening&lt;/li&gt;
&lt;li&gt;Pacing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users earn XP, level up, and track progress on a dashboard.&lt;/p&gt;

&lt;p&gt;### Gemini Vision — The Eyes&lt;/p&gt;

&lt;p&gt;For browser automation, screenshots are sent to Gemini Vision. It understands what's on screen — buttons, forms,&lt;br&gt;
  navigation — and decides where to click, type, and scroll.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mic permissions in Chrome extensions&lt;/strong&gt; — Sidepanels can't access &lt;code&gt;getUserMedia&lt;/code&gt;. I tried 4 approaches before landing&lt;br&gt;
   on a minimal popup window with an AudioWorklet processor streaming PCM audio via Chrome ports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio playback&lt;/strong&gt; — My first approach used &lt;code&gt;onended&lt;/code&gt; callbacks to chain audio buffers. This caused 5-20ms gaps&lt;br&gt;
  between every chunk — speech sounded choppy. The fix: schedule each &lt;code&gt;AudioBufferSource&lt;/code&gt; to start at the exact&lt;br&gt;
  timestamp the previous one ends. Gapless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session timeouts&lt;/strong&gt; — Gemini Live sessions die after ~10 minutes. I built transparent auto-reconnection (up to 20x)&lt;br&gt;
  so conversations can last over an hour without the user noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt; — The API key accidentally got committed to the public repo. I scrubbed it from git history with&lt;br&gt;
  &lt;code&gt;filter-branch&lt;/code&gt;, rotated the key, and moved to OAuth2 short-lived tokens. The key now lives in Cloud Secret Manager&lt;br&gt;
  and never touches client code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Cloud Setup
&lt;/h2&gt;

&lt;p&gt;Cloud Run      → Backend hosting (FastAPI, 2 vCPU, 2GB, autoscale)&lt;br&gt;
  Secret Manager → API key → OAuth2 tokens (60-min expiry)&lt;br&gt;
  Cloud Build    → Docker image CI/CD&lt;br&gt;
  Terraform      → Full IaC (one file, all resources)&lt;/p&gt;

&lt;p&gt;One command deploys everything:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
  ./deploy.sh gaxis-488323

  Architecture

  https://raw.githubusercontent.com/preethamtjit20-spec/gaxis/main/architecture-v3.png

  Try It

  The backend is live:

  curl https://gaxis-132388856648.us-central1.run.app/health
  # {"status":"ok","agent":true}

  Full source + setup instructions: https://github.com/preethamtjit20-spec/gaxis

  ---
  Built for the https://geminiliveagentchallenge.devpost.com/. Your browser already works — G-Axis makes it intelligent.

  ---

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>geminiliveagentchallenge</category>
      <category>gemini</category>
      <category>chromeextension</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
