OpenAI Realtime API Turn Detection & Interruption Errors — Fix Guide (2026)
Realtime API · Turn Detection Severity: High

OpenAI Realtime API Turn Detection & Interruption Errors

Turn detection controls when the model thinks the user is done talking. Get it wrong and you either cut users off mid-sentence or make them wait 5 seconds before the model responds. Interruption (barge-in) has its own settings that trip teams up. Here's the tuning guide and the specific bugs each mode produces.

TL;DRThree modes: server_vad (voice activity detection — fast, threshold-based), semantic_vad (model decides when you're done — slower but smarter for hesitant speakers), and null (manual, you commit the buffer). Set interrupt_response=true to allow barge-in. Tune silence_duration_ms for your users — 200ms is snappy but cuts thinkers off; 500ms is safer for hesitant speech.

Real error messages you'll see

Model cuts user off mid-sentence
Model cuts user off mid-sentence
# User pauses to think mid-sentence; server_vad fires speech_stopped at 200ms;
# model responds before user finishes.
# Fix: raise silence_duration_ms to 400-600ms, or switch to semantic_vad.
Model never responds to short utterances
Model never responds to short utterances
# User says "yes." — too short to trigger VAD threshold.
# Or ambient noise keeps VAD in "speech" state forever.
# Fix: lower VAD threshold; add prefix_padding_ms; check mic gain.
Interruption doesn't work — user talks over model
Interruption doesn't work — user talks over model
# User starts talking while model is speaking; model continues.
# Root cause: interrupt_response=false (default in some versions) or client didn't
# stop feeding audio during model playback.
# Fix: interrupt_response=true; client sends input_audio_buffer.clear on speech_started during response.

Turn detection modes

ModeHow it worksBest for
server_vadThreshold-based: silence for N ms = user doneFast conversations, clear speech
semantic_vadModel decides when user is done based on contentHesitant speakers, dictation, natural pauses
null (manual)You send input_audio_buffer.commitPush-to-talk, controlled environments
Interruptioninterrupt_response=true + client stops feeding audio on speech_startedBarge-in support

Root causes (ranked by frequency)

Based on OpenAI developer reports; percentages sum to 100%.

  • 22%
    silence_duration_ms too short. Default of 200ms cuts thinkers off mid-sentence. Bump to 400-600ms for natural conversation.
  • 18%
    VAD threshold wrong for input. threshold: 0.5 is a starting point. Noisy environments need higher (0.7+); quiet mics need lower (0.3).
  • 14%
    interrupt_response=false. Model keeps talking over the user. Set to true and handle the client-side audio-clear on speech_started.
  • 12%
    Client keeps sending audio during model playback. Even with interrupt_response=true, if the client doesn't stop feeding the user's audio to the model, model won't detect the interruption.
  • 10%
    Manual mode without commit. Set turn_detection=null but forgot to send input_audio_buffer.commit. Server waits forever.
  • 9%
    semantic_vad added latency ignored. Adds ~500-1000ms per turn vs server_vad. Not always the right default despite being "smarter".
  • 8%
    prefix_padding_ms too low. Beginning of utterances gets clipped. Bump to 300-500ms so the first few frames aren't dropped as ambient noise.
  • 7%
    Duplicate response after manual commit. Sent input_audio_buffer.commit then response.create, but auto-response was on — model responds twice.

How to fix it

Fix #1

Tune server_vad for your users — silence duration, threshold, padding

The primary fix for cut-off and delayed responses.

Server VAD has three knobs: threshold (VAD sensitivity, 0-1), prefix_padding_ms (audio kept before speech detected, so first syllables aren't clipped), and silence_duration_ms (how long silence must persist before speech is considered ended). Defaults work for demo conditions; production needs tuning per audience.

vad_tuning.pypython
import json


# ✅ CONVERSATIONAL — snappy responses, clear speakers
CONVERSATIONAL_VAD = {
    "type": "server_vad",
    "threshold": 0.5,               # standard sensitivity
    "prefix_padding_ms": 300,       # 300ms of audio before speech kept (preserves first syllable)
    "silence_duration_ms": 200,     # 200ms silence = user done (snappy)
    "create_response": True,
    "interrupt_response": True,
}


# ✅ HESITANT / THOUGHTFUL SPEAKERS — allow pauses
HESITANT_VAD = {
    "type": "server_vad",
    "threshold": 0.5,
    "prefix_padding_ms": 400,
    "silence_duration_ms": 600,     # 600ms — lets users pause and continue
    "create_response": True,
    "interrupt_response": True,
}


# ✅ NOISY ENVIRONMENT — higher threshold to reject background noise
NOISY_VAD = {
    "type": "server_vad",
    "threshold": 0.7,               # only clear speech triggers
    "prefix_padding_ms": 300,
    "silence_duration_ms": 400,
    "create_response": True,
    "interrupt_response": True,
}


# ✅ TELEPHONY — narrower band, lower fidelity
TELEPHONY_VAD = {
    "type": "server_vad",
    "threshold": 0.5,
    "prefix_padding_ms": 200,       # phone lines add their own latency
    "silence_duration_ms": 500,     # phone users tend to pause more
    "create_response": True,
    "interrupt_response": True,
}


# ✅ Semantic VAD — model decides, slower but handles natural speech
SEMANTIC_VAD = {
    "type": "semantic_vad",
    "eagerness": "medium",          # low = wait longer, high = interrupt sooner
    "create_response": True,
    "interrupt_response": True,
}


# ✅ Manual (push-to-talk) — client controls turn boundaries
MANUAL_TURN = None                  # turn_detection: null


# ✅ Apply the right mode per session context
def session_for_user(user_profile: dict) -> dict:
    if user_profile.get("prefers_slow_pace"):
        return HESITANT_VAD
    if user_profile.get("channel") == "telephony":
        return TELEPHONY_VAD
    if user_profile.get("environment") == "noisy":
        return NOISY_VAD
    return CONVERSATIONAL_VAD


async def configure_session(ws, user_profile: dict):
    await ws.send(json.dumps({
        "type": "session.update",
        "session": {
            "audio": {"input": {"format": "pcm16"}, "output": {"format": "pcm16"}},
            "turn_detection": session_for_user(user_profile),
        },
    }))


# ✅ Adaptive tuning — watch for user complaints or cut-offs, adjust
class AdaptiveVAD:
    def __init__(self):
        self.silence_ms = 300
        self.cutoff_events = 0     # times user restarted after being cut off

    def on_user_restart_within_2s(self):
        """User immediately spoke again after model responded — likely cut off."""
        self.cutoff_events += 1
        if self.cutoff_events >= 3:
            self.silence_ms = min(self.silence_ms + 100, 800)
            self.cutoff_events = 0

    def current_config(self) -> dict:
        return {
            "type": "server_vad",
            "threshold": 0.5,
            "prefix_padding_ms": 300,
            "silence_duration_ms": self.silence_ms,
            "create_response": True,
            "interrupt_response": True,
        }
Note: For general-purpose voice bots, start with silence_duration_ms=300-400, not the demo default of 200. Adjust based on user complaints. Sales / support use cases with patient users tolerate 500-600ms; kiosks and IVR benefit from 200-300ms.
Fix #2

Implement barge-in — interrupt_response=true + client audio-clear

Fixes model talking over the user.

Barge-in requires two things: server-side interrupt_response=true AND client behavior that stops feeding user audio to the model during model playback. When the model is speaking and the user starts talking, the server emits input_audio_buffer.speech_started; the client should immediately clear any queued audio output to the user's speakers.

barge_in_handling.pypython
import json
import base64


class VoicePlaybackController:
    """Manages the user's speaker output; supports mid-playback cancellation."""

    def __init__(self, audio_sink):
        self.sink = audio_sink       # your platform-specific audio output
        self.playing = False
        self.pending_chunks: list[bytes] = []

    async def enqueue(self, audio_bytes: bytes):
        self.pending_chunks.append(audio_bytes)
        if not self.playing:
            self.playing = True
            await self._play_loop()

    async def _play_loop(self):
        while self.pending_chunks and self.playing:
            chunk = self.pending_chunks.pop(0)
            await self.sink.write(chunk)
        self.playing = False

    async def cancel(self):
        """Stop playing immediately — user is interrupting."""
        self.playing = False
        self.pending_chunks.clear()
        await self.sink.flush_and_stop()


# ✅ Full barge-in loop
async def voice_loop(ws, playback: VoicePlaybackController):
    model_is_speaking = False
    current_response_id = None

    async for msg in ws:
        event = json.loads(msg)
        t = event["type"]

        if t == "response.created":
            model_is_speaking = True
            current_response_id = event["response"]["id"]

        elif t == "response.output_audio.delta":
            audio = base64.b64decode(event["delta"])
            await playback.enqueue(audio)

        elif t == "response.done":
            model_is_speaking = False

        elif t == "input_audio_buffer.speech_started":
            # USER IS TALKING — implement barge-in
            if model_is_speaking:
                # 1. Stop playing model audio to user
                await playback.cancel()

                # 2. Tell server how much audio actually reached the user
                # (server needs this to reconstruct what the user heard vs. what was cancelled)
                await ws.send(json.dumps({
                    "type": "response.cancel",
                }))

                model_is_speaking = False

        elif t == "input_audio_buffer.speech_stopped":
            # User done — server_vad auto-triggers response.create if create_response=true
            pass

        elif t == "conversation.item.input_audio_transcription.completed":
            # Whisper transcribed what the user said — good for logging
            print(f"[user said] {event['transcript']}")


# ✅ WebRTC handles barge-in largely automatically
# The browser's peer connection stops playing model audio when the local mic
# picks up user speech, IF you've wired the local stream to the peer connection.
# Just make sure interrupt_response=true in the session config.


# ✅ Handle response.cancelled cleanly
async def on_response_done(event, playback):
    if event.get("response", {}).get("status") == "cancelled":
        # User interrupted; ensure no leftover chunks in playback buffer
        await playback.cancel()

        # Optionally send conversation.item.truncate so the server knows
        # exactly how much audio the user heard
        item_id = event["response"]["output"][-1]["id"] if event["response"]["output"] else None
        if item_id:
            await ws.send(json.dumps({
                "type": "conversation.item.truncate",
                "item_id": item_id,
                "content_index": 0,
                "audio_end_ms": playback.total_played_ms,
            }))
Note: Without conversation.item.truncate after a cancellation, the server thinks the user heard the whole model response even though playback was stopped. Next turn, the model may reference something the user never heard. Sending truncate keeps the model's mental model of the conversation aligned with reality.
Fix #3

Semantic VAD when threshold-based fails — natural pauses, dictation

When to switch modes.

semantic_vad uses the model itself to decide when a user is done speaking based on content — a trailing "um..." or a question that hasn't been asked yet keeps the turn open. Trade-off: adds ~500-1000ms latency per turn vs threshold-based. Use it for hesitant users, dictation flows, or any case where natural pauses would trip server_vad.

semantic_vad_config.pypython
# ✅ Semantic VAD — model decides based on content
SEMANTIC_VAD_SESSION = {
    "type": "session.update",
    "session": {
        "audio": {"input": {"format": "pcm16"}, "output": {"format": "pcm16"}},
        "turn_detection": {
            "type": "semantic_vad",
            "eagerness": "medium",     # "low" | "medium" | "high" | "auto"
            "create_response": True,
            "interrupt_response": True,
        },
    },
}


# eagerness values:
#   "low"    — waits longer; best for slow speakers, minimizes false cuts
#   "medium" — balanced; good default for most conversational apps
#   "high"   — cuts sooner; feels snappy but may interrupt thinkers
#   "auto"   — model picks based on session context


# ✅ Manual turn detection — push-to-talk, controlled environments
MANUAL_SESSION = {
    "type": "session.update",
    "session": {
        "audio": {"input": {"format": "pcm16"}, "output": {"format": "pcm16"}},
        "turn_detection": None,       # null = manual
    },
}


async def push_to_talk(ws, mic_stream):
    """User holds a button; we send audio; on release we commit."""
    await ws.send(json.dumps(MANUAL_SESSION))

    async for pcm_chunk in mic_stream:
        await ws.send(json.dumps({
            "type": "input_audio_buffer.append",
            "audio": base64.b64encode(pcm_chunk).decode("ascii"),
        }))

    # User released the button
    await ws.send(json.dumps({"type": "input_audio_buffer.commit"}))
    await ws.send(json.dumps({"type": "response.create"}))


# ✅ Hybrid — start with semantic_vad, downgrade if latency complaints mount
class TurnDetectionAdapter:
    def __init__(self):
        self.mode = "semantic_vad"
        self.latency_samples: list[float] = []

    def record_turn_latency(self, seconds: float):
        self.latency_samples.append(seconds)
        # Keep last 20 samples
        self.latency_samples = self.latency_samples[-20:]

    def should_downgrade(self) -> bool:
        """If p50 latency > 1.5s, drop to server_vad."""
        if len(self.latency_samples) < 5:
            return False
        sorted_lat = sorted(self.latency_samples)
        p50 = sorted_lat[len(sorted_lat) // 2]
        return p50 > 1.5

    async def maybe_switch(self, ws):
        if self.mode == "semantic_vad" and self.should_downgrade():
            self.mode = "server_vad"
            await ws.send(json.dumps({
                "type": "session.update",
                "session": {
                    "turn_detection": {
                        "type": "server_vad",
                        "silence_duration_ms": 300,
                        "create_response": True,
                        "interrupt_response": True,
                    },
                },
            }))


# ✅ Common misconception: semantic_vad + interrupt_response
# Semantic VAD DOES support interrupt_response=true. The model can decide
# both "user is done speaking" AND "user is interrupting me" from content.
# It's not either/or.
Note: Semantic VAD is worth the latency cost when your users are non-native speakers, elderly, or in accessibility-focused deployments — the model is much more forgiving of natural pauses. For rapid consumer chat, threshold-based server_vad with tuned silence_duration_ms usually wins.

Prevention checklist

  • Start with silence_duration_ms=300-400, not the demo default 200. Adjust based on your users.
  • Set interrupt_response=true AND handle the client-side audio-clear on speech_started. Both are needed for barge-in.
  • After a cancelled response, send conversation.item.truncate so the server knows what the user actually heard.
  • Use semantic_vad for hesitant speakers, dictation, accessibility — at ~1s added latency.
  • For push-to-talk, turn_detection=null and manually send input_audio_buffer.commit then response.create.
  • Tune threshold for the environment — noisy: 0.7+; quiet mic: 0.3.
  • Bump prefix_padding_ms to 300-500 so the first syllable of each utterance isn't clipped.

Frequently asked questions

What's the difference between server_vad and semantic_vad in latency?

Server VAD adds only silence_duration_ms (typically 200-500ms) between the user's last word and the response starting. Semantic VAD adds that plus a model round-trip to decide "is the user done?" — usually 500-1000ms more. For conversational apps, server VAD feels snappier; for dictation and thoughtful conversations, semantic VAD feels more natural despite being slower.

Why does the model interrupt me when I pause?

Your silence_duration_ms is shorter than your natural pauses. If you pause 350ms to think and silence_duration_ms is 200ms, the model thinks you're done. Raise silence_duration_ms to 500-600ms, or switch to semantic_vad which recognizes an incomplete thought.

Can barge-in work over telephony?

Yes with interrupt_response=true and G.711 audio, but telephony has its own echo suppression that may swallow the user's barge-in speech. Verify that user audio actually reaches your Realtime WebSocket bridge (Twilio Media Streams pass it through). If interrupts still don't register, lower the VAD threshold on the user side.

What is eagerness on semantic_vad?

It controls how eagerly the model ends a turn. "low" waits longer before deciding you're done (best for slow speakers); "high" ends turns snappier; "medium" and "auto" balance. Higher eagerness reduces latency but risks cutting slow speakers off. Start with "medium" and adjust based on user feedback.

How do I make the model wait for a specific phrase before responding?

Combine manual turn detection (turn_detection: null) with client-side keyword detection. Your client transcribes locally (e.g. with a small Whisper model or a browser API) and only sends input_audio_buffer.commit when it detects the trigger phrase. This is the "Hey Assistant" pattern — wake word local, conversation via Realtime.

Related errors