# Arbitrary WebSockets on Agent Runtime with BYOC In [The Agent Platform API contract](/agents/ap-api-contract/) and [Serving the Agent Platform API contract](/agents/serving-the-ap-api-contract/), I covered how Agent Runtime dispatches `:query` and `:streamQuery` calls to the two fixed routes in a bring-your-own-container (BYOC) image: `/api/reasoning_engine` and `/api/stream_reasoning_engine`. Comment: This post was generated with Gemini 4 Argon, based on research I was doing into websocket streaming. I believe it to be accurate based on documented platform capabilities and my existing knowledge, but I have not yet tested it out myself. –William What if your client speaks its own WebSocket wire format—like a browser voice client, a custom mobile app, or a telephony stream—and you want it to connect straight to your agent? Or what if your sessions run longer than 10 minutes? With a container or Dockerfile deployment on Agent Runtime, the runtime exposes an underlying API passthrough that forwards arbitrary HTTP and WebSocket routes directly to your container unchanged, with a 60-minute connection ceiling. ## Why `:bidiStreamQuery` Isn't Always What You Want When you deploy an agent using the managed object path, Agent Runtime builds the serving layer around your Python object and exposes bidirectional streaming through [`:bidiStreamQuery`](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/bidirectional-streaming) (currently in Preview, and only with the `EXPERIMENTAL` `agent_server_mode`). Because the platform owns the server, two constraints apply: 1. **Fixed wire format:** You connect with the GenAI SDK's live client, select the agent method via `class_method`, and every reply comes back wrapped as `{"bidiStreamOutput": {"output": ...}}`—there's no way to serve your own schema. 2. **10-minute stream limit:** Connections on `:bidiStreamQuery` have a [documented maximum timeout of 10 minutes](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/bidirectional-streaming). That works well for short interactive sessions when you control the client and are calling the Agent Platform SDK. It doesn't work when the caller has a fixed WebSocket schema of its own or needs sessions that run past 10 minutes—you'd otherwise have to run a separate adapter service just to translate payloads into `:bidiStreamQuery`'s JSON format. Bring your own container, and the plumbing is yours. ## Two Ways to Stream into Agent Runtime Agent Runtime provides two distinct ingress paths depending on how you package and address your agent: | Feature | Managed Contract (`:query` / `:streamQuery` / `:bidiStreamQuery`) | Underlying API Passthrough (`/api/**` & `/ws/v1/.../api/**`) | |---|---|---| | **Deployment Type** | Object, source, Dockerfile, or prebuilt container (BYOC); `:bidiStreamQuery` is managed-serving only (Preview) | Dockerfile or prebuilt container (BYOC) | | **Container Routes** | Fixed (`POST /api/reasoning_engine`, `POST /api/stream_reasoning_engine`) | Any custom HTTP or WebSocket route your server defines | | **Wire Format** | Platform RPC envelope (`class_method` dispatch, `bidiStreamOutput` framing) | Arbitrary JSON, text, or binary WebSocket frames | | **WebSocket Timeout** | 10 minutes (`:bidiStreamQuery`) | Up to 60 minutes (`bidiInvokeReasoningEngine`) | | **REST / RPC Method** | `reasoningEngines.query` / `streamQuery` / `bidiStreamQuery` | `invokeReasoningEngine` (HTTP) / `bidiInvokeReasoningEngine` (WebSocket) | ## How the WebSocket Passthrough Works Alongside the standard `:query` and `:streamQuery` methods, Agent Runtime exposes a passthrough surface documented under [Use deployed agents through their underlying API](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/use-an-agent#use_deployed_agents_through_their_underlying_api). In the [`reasoningEngines.ws.v1` REST reference](https://docs.cloud.google.com/gemini-enterprise-agent-platform/reference/rest/v1/reasoningEngines.ws.v1.projects.locations.reasoningEngines.revisions.runtimeRevisions.api) and [Agent Platform RPC reference](https://docs.cloud.google.com/gemini-enterprise-agent-platform/reference/rpc), these passthrough operations are named `invokeReasoningEngine` (for HTTP) and `bidiInvokeReasoningEngine` (for WebSockets)—though as a caller, you never put those method names in the URL yourself. Instead, Google Cloud's API frontend maps any request matching the `/api/**` wildcard path directly to them.
A WebSocket client connects to the public endpoint with a Google OAuth access token identifying the calling user or service account. The URL automatically selects Agent Runtime's bidiInvokeReasoningEngine operation, which checks IAM and forwards /voice/stream to the BYOC container's FastAPI endpoint on $PORT, default 8080. Text, JSON, and binary data flow in both directions.
Scroll horizontally to explore. Open full-size diagram
Every path under `/api/` belongs to your container. When you connect to the `/reasoningEngines/ws/v1/.../api/**` endpoint, Agent Runtime authenticates the request with IAM, strips the prefix up through `/api`, and upgrades the connection directly against your container on `$PORT` (default `8080`): ```text Local container: ws://localhost:8080/voice/stream Deployed on Agent Runtime: wss://LOCATION-aiplatform.googleapis.com/reasoningEngines/ws/v1/projects/PROJECT_NUMBER/locations/LOCATION/reasoningEngines/RESOURCE_ID/api/voice/stream ``` For standard HTTP routes (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`), the equivalent passthrough path is `/reasoningEngines/v1/projects/PROJECT_NUMBER/locations/LOCATION/reasoningEngines/RESOURCE_ID/api/`. ## Breaking Past the 10-Minute Streaming Limit In a voice agent architecture, there are two separate WebSocket legs to keep in mind—and BYOC lets you run sessions up to **60 minutes**: 1. **Inbound leg (Client ↔ Agent Runtime container):** Unlike `:bidiStreamQuery`'s [10-minute cap](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/bidirectional-streaming), `bidiInvokeReasoningEngine` has a 60-minute connection ceiling. Your caller's WebSocket connection to Agent Runtime stays open for up to an hour. 2. **Outbound leg (Agent container ↔ Gemini Live API):** Under the hood, the Gemini Live API enforces its own ~10-minute WebSocket connection limit (and a 15-minute default audio session cap). Because your container owns the inbound connection, you can enable `session_resumption` and `context_window_compression` in ADK's [Bidi-streaming `RunConfig`](https://adk.dev/live/configuration/) so ADK transparently reconnects to Gemini Live in the background without ever dropping the caller's inbound WebSocket. ## Serving Both in One Container You don't have to choose between a custom WebSocket interface and the [Agent Platform runtime contract](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/runtime-contract). The same FastAPI app can serve `/api/reasoning_engine` and `/api/stream_reasoning_engine`—so the Google Cloud console playground, the Python SDK, and Gemini Enterprise continue to work—while exposing a raw WebSocket endpoint right beside them: ```python from fastapi import FastAPI, WebSocket, WebSocketDisconnect from google.adk.agents.run_config import RunConfig from google.genai import types app = FastAPI() # Keep outbound Gemini Live sessions running seamlessly past 10-15 minutes run_config = RunConfig( session_resumption=types.SessionResumptionConfig(transparent=True), context_window_compression=types.ContextWindowCompressionConfig( trigger_tokens=100000, sliding_window=types.SlidingWindow(target_tokens=80000), ), ) @app.get("/") async def health(): return {"status": "ok"} # Arbitrary WebSocket endpoint (up to 60m) reached via: # wss://-aiplatform.googleapis.com/reasoningEngines/ws/v1/{name}/api/voice/stream @app.websocket("/voice/stream") async def voice_stream(ws: WebSocket): await ws.accept() try: while True: # Receive raw text or binary frames in any protocol message = await ws.receive_text() # Forward to ADK LiveRequestQueue / run_live(run_config=run_config) await ws.send_text(message) except WebSocketDisconnect: pass ``` Deploy the image using `spec.containerSpec.imageUri` (Option 5 in [5 ways to deploy agents on Agent Platform](/agents/5-ways-to-deploy/)) or let Agent Runtime build your `Dockerfile` from source (Option 4). ## Two Gotchas Worth Calling Out * **The `/api/` segment in the public URL is mandatory:** On the public URL, `/api/` marks the boundary between the `reasoningEngine` resource name and the path forwarded to your container. A request to `.../reasoningEngines/RESOURCE_ID/api/voice/stream` arrives at your container as `/voice/stream` (unless you also mount your routes under `/api` in FastAPI, so watch your prefixes). * **IAM still guards the front door:** Agent Runtime enforces Google Cloud IAM (`aiplatform.reasoningEngines.query`) on the HTTP `Upgrade: websocket` handshake, before the connection ever reaches your container. Any backend service you control can mint a Google Cloud OAuth token (via ADC, Workload Identity Federation, or a service account) and pass `Authorization: Bearer ` in the initial upgrade headers—but if your caller can't set custom HTTP headers on the WebSocket upgrade (like a browser `new WebSocket()` client, or a third-party media stream that only passes credentials in query parameters or post-connect JSON frames), that's the one reason you'd still keep a lightweight auth proxy in front.