Most developers building with large language models assume that production reliability is a model alignment problem. When an interactive web or mobile agent hangs mid-task, the default reaction is to tweak the system prompt, adjust temperature, or swap model checkpoints.
In production client architectures, however, over 80% of perceived agent failures have nothing to do with prompt quality or model hallucinations. They are silent client-side transport crashes: dropped WebSocket and HTTP/2 frames during multi-second reasoning pauses, in-memory buffer thrashing when streaming large token contexts into frontend state, and orphaned tool side-effects caused by uncommitted connection boundaries.
Firebase AI Logic vs. Cloud Run: The full-stack topology
A common point of confusion is whether to execute agent loops entirely on the client or route through a custom backend container. In production architectures, they form the two complementary halves of a resilient runtime:
Manages direct token streaming to the user viewport, cryptographically validates device health via Firebase App Check, and throttles UI rendering at 60fps to eliminate DOM thrashing.
Executes multi-step tool calls, guards private API keys, persists state to Cloud Firestore, and emits synthetic keep-alive heartbeat frames during deep model reasoning.
The three production streaming failure modes
When an agent executes multi-step reasoning or calls external tools, the HTTP/2 or WebSocket connection remains open for 10 to 45 seconds while tokens stream sequentially. This opens three distinct failure modes:
The intermediate NAT / proxy idle timeout
During deep reasoning loops or multi-tool tool calling sequences, the backend model may take 6 to 12 seconds before emitting the next token chunk. Cellular radio handoffs, corporate firewalls, and ingress reverse proxies (like Envoy or Cloud Load Balancing) aggressively terminate HTTP/2 streams (IETF RFC 9113) that show zero wire activity for >10 seconds. The frontend receives an ECONNRESET or silent EOF, leaving the user staring at an unresponsive loading skeleton.
Hold a streaming conversation open and watch NAT idle timeouts silently drop it - then watch keep-alives and idempotent resume save it.
In-memory component state thrashing
Naive frontend implementations bind the raw streaming chunk handler directly to reactive framework state:
// ❌ ANTI-PATTERN: Re-rendering 50-file diffs on every token chunk
onChunk((chunk) => {
setMessages((prev) => [...prev.slice(0, -1), prev.at(-1) + chunk]);
});When an agent streams a 65k-token code diff or architectural review, updating React or Vue virtual DOM nodes 40 times per second triggers massive memory garbage collection spikes, dropping frames on mobile viewports and frequently crashing client tab processes.
The orphaned tool state execution
If the network connection drops while the server is executing Step 3 of a 4-step tool chain (for example, creating a Cloud Firestore document before calling an external Stripe webhook), the client assumes the entire operation failed and automatically resubmits Turn 1. Without client-enforced idempotency keys, this produces duplicate database writes and inconsistent backend state.
The three-layer client-edge defense architecture
Production agent runtimes require a structured separation between network transport, local persistence, and viewport rendering:
Cloud Run emits lightweight SSE heartbeat comment frames (:keep-alive\n\n) every 4 seconds to keep intermediate TCP sockets active during deep reasoning, while the client tracks byte-level stream offsets (X-Stream-Resume-Offset) for instant reconnection.
Decouples the raw socket byte reader from the UI rendering engine. Incoming chunks stream into an in-memory buffer, dispatching throttled updates to the virtual DOM at a smooth 60fps render tick to prevent client memory GC spikes.
Authenticates client device integrity with Firebase App Check JWT tokens, and binds each multi-turn request to atomic Firestore transaction IDs, verifying whether previous turns executed before retrying.
Client implementation: Resilient stream consumer
Below is the production TypeScript implementation for client-side streaming using Firebase App Check and exponential backoff reconnection:
import { getToken } from "firebase/app-check";
/**
* Resilient Stream Consumer with Reconnection Offsets & App Check Attestation
*/
export class ResilientAgentConsumer {
private resumeOffset = 0;
private maxRetries = 3;
constructor(
private readonly endpoint: string,
private readonly appCheckInstance: any,
private readonly turnId: string
) {}
public async executeStream(
prompt: string,
onRenderTick: (text: string) => void
): Promise<void> {
let attempt = 0;
let accumulatedText = "";
while (attempt < this.maxRetries) {
try {
// Fetch fresh App Check token with offline-safe fallback
let appCheckToken = "";
try {
appCheckToken = (await getToken(this.appCheckInstance, false)).token;
} catch (tokenErr) {
throw new Error(`App Check attestation failed (offline or unverified): ${tokenErr}`);
}
const response = await fetch(this.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Firebase-AppCheck": appCheckToken,
"X-Stream-Turn-Id": this.turnId,
"X-Stream-Resume-Offset": String(this.resumeOffset),
},
body: JSON.stringify({ prompt, resumeFrom: this.resumeOffset }),
});
if (!response.ok || !response.body) {
throw new Error(`HTTP Transport Failure: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let lineBuffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
lineBuffer += decoder.decode(value, { stream: true });
const lines = lineBuffer.split(/\r?\n/);
lineBuffer = lines.pop() || "";
for (const line of lines) {
// Filter out W3C SSE comment frames (:keep-alive) even when packet fragmented
if (line.startsWith(":") || !line.trim()) continue;
accumulatedText += line + "\n";
}
this.resumeOffset += value.byteLength;
// Throttled UI dispatch to prevent client memory GC thrashing
onRenderTick(accumulatedText);
}
// Stream completed successfully
return;
} catch (err) {
attempt++;
if (attempt >= this.maxRetries) {
throw new Error(`Agent stream terminated after ${this.maxRetries} retries: ${err}`);
}
// Exponential backoff with jitter before resuming from last byte offset
const backoffMs = Math.pow(2, attempt) * 500 + Math.random() * 200;
await new Promise((res) => setTimeout(res, backoffMs));
}
}
}
}Backend gateway on Cloud Run (Node.js and Firebase Admin)
Below is the production Express middleware running on Cloud Run, enforcing device attestation via Firebase App Check and emitting RFC-compliant keep-alive comment frames to prevent reverse-proxy timeouts:
import { Request, Response, NextFunction } from "express";
import { getAppCheck } from "firebase-admin/app-check";
/**
* Cloud Run Middleware: App Check Token Verification
*/
export async function verifyAppCheckMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const appCheckToken = req.header("X-Firebase-AppCheck");
if (!appCheckToken) {
return res.status(401).json({ error: "Unauthorized: Missing App Check token" });
}
try {
const claims = await getAppCheck().verifyToken(appCheckToken);
(req as any).appCheckClaims = claims;
next();
} catch (err) {
return res.status(401).json({ error: "Unauthorized: Invalid App Check token" });
}
}
/**
* Configures Cloud Run Streaming Headers & Heartbeat Keep-Alives
*/
export function setupStreamingHeaders(res: Response): NodeJS.Timeout {
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no"); // Disable proxy buffering on Cloud Run / Envoy
// Emit SSE keep-alive heartbeat comment frame every 4 seconds
const heartbeatTimer = setInterval(() => {
if (!res.writableEnded) {
res.write(":keep-alive\n\n");
}
}, 4000);
res.on("close", () => clearInterval(heartbeatTimer));
res.on("finish", () => clearInterval(heartbeatTimer));
return heartbeatTimer;
}:keep-alive\n\n) to preserve long streaming sessions.Primary research and documentation
- IETF RFC 9113: HTTP/2 Standard (Stream Multiplexing and Flow Control)
- WHATWG HTML Standard: Server-Sent Events (SSE) Protocol
- WHATWG Streams Standard: ReadableStream and Backpressure Handling
- Firebase App Check Overview and Attestation Architecture
- Google Cloud Run Response Streaming Configuration
- Cloud Firestore Transactions and Batched Writes
