Skip to content

Live API: a session that exchanges a turn records a 409 Conflict; an empty one does not #1881

Description

@XeniaLu

What happens

Every Live API session that exchanges a turn records exactly one 409 Conflict on the "API errors" chart of the AI Studio usage dashboard. A session that connects and closes without sending anything records none - that is the line between the two, and it is reproducible in both directions.

Each session is recorded as exactly one request. For a session that exchanged a turn, that request is the 409; for one that exchanged nothing, the same request is recorded as a success. So the error rate for a key doing real work sits at 100%.

The sessions themselves work. Audio flows, transcripts arrive, tool calls arrive, usage and token counts are recorded, and the WebSocket closes with code 1000. Nothing surfaces to the client and there is no error body anywhere. The dashboard is the only sign.

Minimal reproduction

Connect, send one text turn, wait for turnComplete, close. This records one 409. Removing the sendClientContent call - so the session only connects and closes - records none.

import { GoogleGenAI, Modality } from '@google/genai';

async function main(): Promise<void> {
	const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });

	let turnComplete = false;

	const session = await ai.live.connect({
		model: 'models/gemini-3.1-flash-live-preview',
		config: {
			responseModalities: [Modality.AUDIO],
			systemInstruction: 'You are a terse assistant. Keep every reply to one short sentence.',
		},
		callbacks: {
			onopen: () => console.log('open', new Date().toISOString()),
			onmessage: (message) => {
				if (message.serverContent?.turnComplete) {
					turnComplete = true;
					console.log('turn complete', new Date().toISOString());
				}
			},
			onerror: (event) => console.log('error', event.message),
			onclose: (event) => console.log('close', event.code, event.reason, new Date().toISOString()),
		},
	});

	session.sendClientContent({ turns: [{ role: 'user', parts: [{ text: 'Say hello.' }] }], turnComplete: true });

	for (let waited = 0; waited < 30_000 && !turnComplete; waited += 200) {
		await new Promise((resolve) => setTimeout(resolve, 200));
	}

	session.close();

	// Give the close handshake time to finish before the process exits.
	await new Promise((resolve) => setTimeout(resolve, 3000));
}

void main();

Run it once, then read the "API errors" chart for that minute. The dashboard reflected it within a minute or two.

What was measured

Ten sessions, run standalone unless noted:

Session What it did 409 recorded
3 x Connected, closed. Nothing sent. none
2 x Sent one text turn, waited for turnComplete, closed. one each
1 Sent one text turn, closed. Run in a process alongside two others. one
1 Declared a hang_up function, prompted the model to call it, answered the call with sendToolResponse, closed. one
2 Full two-way audio conversations of two and three minutes. one each

Six sessions that exchanged a turn, six conflicts. Three that exchanged nothing, none. Every close was code 1000 with an empty reason, and every session appeared on the request chart exactly once.

What this rules out

  • Not the tool call. A plain text turn is enough. The session that answered a hang_up call with sendToolResponse before closing behaves exactly like the ones with no tools declared.
  • Not closing while the model is mid-turn. The repro waits for turnComplete before closing. A separate test waited 5 further seconds beyond that, and still recorded a conflict.
  • Not one session colliding with the one before it. Two empty sessions opened 25 seconds apart recorded nothing at all, and the conversational sessions were minutes apart from anything else.
  • Not the server rejecting or dropping the connection. Every close event is code 1000 with an empty reason - the client's own close, acknowledged normally.

Waiting does not end a session either: 5 seconds after the final turn there was still no close event, so the client is the one that has to close it.

Environment

  • @google/genai 2.17.1
  • Node.js 22.23.2
  • Model models/gemini-3.1-flash-live-preview
  • Gemini API key (AI Studio), not Vertex

Questions

  1. What does a 409 mean on the Live API, and what raises it? Nothing in a session's own lifecycle appears to fail, and a session that says nothing does not raise it at all.
  2. Is a client-initiated session.close() the expected way to end a session that has been used? If not, what is?
  3. Does this count against quota, rate limits, or any health signal? An error logged for every session that does real work leaves nothing to alert on - a genuine failure looks the same as a normal day.

Related

Metadata

Metadata

Assignees

Labels

api:gemini-apipriority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions