惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
V
Visual Studio Blog
The GitHub Blog
The GitHub Blog
Apple Machine Learning Research
Apple Machine Learning Research
J
Java Code Geeks
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
人人都是产品经理
人人都是产品经理
H
Heimdal Security Blog
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
Cyberwarzone
Cyberwarzone
Google DeepMind News
Google DeepMind News
雷峰网
雷峰网
Hacker News: Ask HN
Hacker News: Ask HN
Webroot Blog
Webroot Blog
Microsoft Azure Blog
Microsoft Azure Blog
MyScale Blog
MyScale Blog
A
About on SuperTechFans
V2EX - 技术
V2EX - 技术
小众软件
小众软件
博客园 - Franky
博客园 - 司徒正美
P
Privacy International News Feed
爱范儿
爱范儿
U
Unit 42
博客园 - 叶小钗
The Hacker News
The Hacker News
C
Check Point Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
N
News and Events Feed by Topic
D
Docker
T
Threatpost
MongoDB | Blog
MongoDB | Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
H
Help Net Security
L
LINUX DO - 最新话题
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
A
Arctic Wolf
Spread Privacy
Spread Privacy

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
The 1.4 Seconds That Weren't on Any Span
Marcus Chen · 2026-06-25 · via DEV Community

On the morning of June 3rd, a customer on a live call sat through 1.4 seconds of dead air after she finished a sentence, long enough that she said "hello?" before the agent answered. I had the trace open in Honeycomb forty seconds later. Every span was green. End-to-end p95 read 980ms, comfortably under our budget, and not one span in that waterfall was longer than 400ms. The dashboard told me everything was fine while the customer was, in fact, talking to silence.

TL;DR: End-to-end voice latency is not the sum of your spans. The number that kills your UX lives in the unattributed time between spans, most often the gap between turn-end (the moment the user stops talking) and ASR-start (the moment your pipeline begins transcribing). APM-style tracing instruments the work and ignores the waiting, so the gap is invisible by construction. You have to put a span on the handoff itself.

Here is the thing I keep saying and keep being right about: most "LLM observability" is just APM with extra steps. It watches the model. It traces the LLM call, the tool call, the retrieval, the token count, all the parts a backend engineer already knows how to think about. For a voice agent that is the wrong half of the system. Voice agents do not break inside the LLM call. They break in the audio pipeline, in the orchestration between components, in the handoffs nobody owns a span for. Your model can be fast and your product can still feel broken, and your dashboard will not say a word about it.

What the dashboard showed

Our turn looks like this on paper. VAD/turn-detection decides the user is done. Audio goes to ASR (Whisper Large v3, streaming). The transcript goes to the LLM (gpt-4o-realtime) for a first token, then the full response. The response streams to TTS (ElevenLabs) for the first audio byte, which is the moment the user hears anything. There is network on both ends.

I pulled the one trace from the 1.4-second call. Not an aggregate, the actual trace. Here is the latency budget I had been staring at for weeks, the summed-span view:

Stage p50 p95 p99 who owns the span
VAD / turn-detection 60ms 120ms 180ms orchestrator
ASR (streaming) 180ms 310ms 540ms ASR client
LLM TTFT 220ms 380ms 720ms model client
LLM full response 140ms 260ms 430ms model client
TTS first byte 90ms 190ms 360ms TTS client
Network (both legs) 40ms 90ms 150ms gateway

Add the p95 column. It comes to roughly 1340ms. Our reported end-to-end p95 was 980ms (the percentiles do not stack, a single request rarely hits the tail on every stage at once, so the real end-to-end p95 sits below the naive sum). Fine. Either way, both numbers are wrong about the call that paged me, because the call that paged me had 1.4 seconds the table does not contain. None of these rows is the dead air. The dead air is the white space between two of them.

Pulling the one trace

When you look at a single voice turn in a normal tracing UI, you get a waterfall of bars. Each bar is a span. The instinct, the APM instinct, is to find the longest bar and optimize it. I spent two days doing exactly that. I made ASR faster. I shaved 40ms off TTFT with a prompt cache. The summed bars got shorter and the dead air did not move, because the dead air was never a bar.

Here is the timeline I finally drew on a whiteboard, because the tracing UI would not draw it for me.

Voice-turn latency waterfall: spans start 1400ms after turn-end

Figure: one voice turn. The captured spans are short and correct, but they start 1400ms late. The damage is the unattributed gap to their left.

That bracket on the left is the whole post. The spans were honest. They were short, they were green, they summed to a healthy number. They just started 1.4 seconds after the user stopped talking, and nothing in the trace measured the wait, because the code path between "turn-detection fired" and "ASR client opened a stream" did not open a span. It awaited a coroutine, hit a connection-pool stall under load, and sat there. Silent. Unspanned. Invisible.

Root cause

The turn-detection callback handed off to ASR through a queue, and the ASR client lazily established its streaming connection on first use. Under concurrent calls, that connection setup contended on a pool that was sized for steady state, not for the moment six calls all finished a turn inside the same 200ms window. So turn-end fired, the handoff coroutine queued the audio, and then waited on a connection that was busy being born. By the time the ASR span opened, 1.4 seconds had passed. The ASR span itself then ran in 300ms, green and blameless.

The fix is two parts. Put a span around the handoff so the gap stops being invisible. Then fix the pool. You cannot fix what you cannot see, and the entire reason this lived in production for weeks is that the gap was never a measurable thing.

Here is the real instrumentation. This is OpenTelemetry Python, opentelemetry-api and opentelemetry-sdk, the actual SDK calls, runnable.

from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode

tracer = trace.get_tracer("voice.turn")


async def handle_turn(audio_in, ctx):
    # The outer span is the whole turn, anchored at turn-end.
    with tracer.start_as_current_span(
        "voice.turn",
        kind=SpanKind.SERVER,
    ) as turn_span:
        turn_span.set_attribute("call.id", ctx.call_id)
        turn_span.set_attribute("turn.index", ctx.turn_index)

        # THE MISSING SPAN: turn-detection -> ASR-start handoff.
        # Everything that happens between "user stopped talking" and
        # "ASR actually began" gets measured here, including the wait
        # for a streaming connection that used to be invisible.
        with tracer.start_as_current_span("voice.handoff.vad_to_asr") as hs:
            hs.set_attribute("handoff.from", "turn_detection")
            hs.set_attribute("handoff.to", "asr")
            try:
                asr_stream = await asr_client.open_stream(ctx)
            except Exception as exc:
                hs.set_status(Status(StatusCode.ERROR, str(exc)))
                hs.record_exception(exc)
                raise
            # mark when audio truly starts flowing into ASR
            hs.add_event("asr_stream_ready")

        # ASR itself. Short and green. Never the problem.
        with tracer.start_as_current_span("voice.asr") as asr_span:
            transcript = await asr_stream.transcribe(audio_in)
            asr_span.set_attribute("asr.transcript_chars", len(transcript))

        # LLM and TTS spans continue as before.
        with tracer.start_as_current_span("voice.llm") as llm_span:
            reply = await llm_client.complete(transcript, ctx)
            llm_span.set_attribute("llm.model", ctx.model)

        with tracer.start_as_current_span("voice.tts") as tts_span:
            first_byte = await tts_client.first_audio_byte(reply)
            tts_span.set_attribute("tts.first_byte_ms", first_byte.elapsed_ms)

        return reply

The point is the voice.handoff.vad_to_asr span. It wraps the dead zone between two components that each had their own span and were each, individually, fast. Now the wait has a name and a duration. The next time six calls finish a turn at once, the handoff span balloons to 1400ms and the connection-pool stall is right there in the waterfall instead of hiding in the white space.

And once the span exists, you can query for it. Here is the trace query I now run, written for a backend that speaks SQL-ish over spans (Honeycomb's query builder maps to the same idea, and so does any OTLP store you can point at ClickHouse). It surfaces turns where the handoff alone blew past 250ms:

SELECT
  trace_id,
  call_id,
  duration_ms AS handoff_ms
FROM spans
WHERE name = 'voice.handoff.vad_to_asr'
  AND duration_ms > 250
ORDER BY handoff_ms DESC
LIMIT 50;

That query returns nothing on a normal day and lights up the instant the pool starts contending. I wired it to an alert on the handoff span's p95, not the end-to-end p95, because the end-to-end p95 is exactly the number that lied to me on June 3rd.

The pool fix was unglamorous. Pre-warm the ASR streaming connections, size the pool for burst concurrency instead of average, and keep the connections alive between turns instead of opening lazily. Handoff p95 went from 1400ms on the bad call down to 70ms steady-state. The dead air was gone the same afternoon I shipped the span, because the span told me precisely where to put the fix.

What this does NOT solve

Instrumenting the handoff makes the gap visible. It does not make your infrastructure fast. A few honest limits.

It does not fix jitter under load on its own. The span tells you the handoff is slow, but if your pool, your event loop, or your GC is the bottleneck, you still have to go fix that. The span is a flashlight, not a wrench.

It does nothing about provider-side queueing you cannot see. When ElevenLabs or your ASR vendor queues your request on their side, your client-side span measures the wait but cannot attribute it past the boundary. You will know that you waited, not why the provider made you wait. For that you need their status, their rate-limit headers, sometimes a support ticket.

And it will not catch every gap automatically. I added the VAD-to-ASR span because that is where this fire was. There are other handoffs (ASR-to-LLM, LLM-to-TTS, barge-in cancellation) and each one needs its own span if you want to see its gap. Instrument the ones that hurt first.

Lesson: Instrument the handoffs, not just the calls. A green waterfall of short, correct spans can still add up to a customer saying "hello?" into silence, because the damage is the time between the bars, and a trace only shows you the bars you drew. The day I stopped trusting the summed p95 and started putting spans on the gaps is the day the dead air stopped paging me. If you run voice agents, go find your turn-end-to-ASR-start handoff right now, wrap it in a span, and alert on that span alone. It is the cheapest 1.4 seconds you will ever buy back.