# 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.
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.