Module 10: AI Backend & Workflow Engineering Lesson 29 / 41
45–50 min read Level: Advanced
Part 4 — AI Backend & Workflow Engineering

Lesson 16.29: Streaming, Webhooks & Real-Time AI Workflows

Server-Sent Events (SSE), WebSockets, Event Delivery, Background Progress Updates, and Live Agent Responses — the comprehensive architectural guide to eliminating opaque waiting spinners with real-time feedback.

What You Will Learn

  • 1. Explain structural trade-offs between Streaming, Polling, SSE, and WebSockets.
  • 2. Stream AI model completions using FastAPI StreamingResponse.
  • 3. Consume real-time streaming events from OpenAI Responses API.
  • 4. Build a chunk-boundary safe SSE parser in React/TypeScript.
  • 5. Architect live progress updates from background Celery jobs to the UI.
  • 6. Master durability distinctions between Redis Pub/Sub and Redis Streams.
  • 7. Construct secure webhooks with HMAC-SHA256 cryptographic signatures.
  • 8. Strictly defend against SSRF attacks on customer-configured webhook URLs.
  • 9. Manage event deduplication, reconnection, and Last-Event-ID replay.
  • 10. Stream multi-stage execution events using OpenAI Agents SDK.
  • 11. Build an end-to-end real-time AI document analyzer project.
  • 12. Audit a 12-point enterprise production readiness checklist.

01 Deconstructing the Problem: The Danger of Silent Waiting in AI

Over the previous two lessons, we established a resilient backend foundation: clean layered architecture (Lesson 27) and Celery + Redis background queuing with retry engineering (Lesson 28). However, from the user experience perspective, a critical architectural void remains.

Consider an enterprise analyst in Byteion SaaS uploading a 200-page financial statement and asking:

“Extract all hidden financial risk vectors and statutory compliance violations across this document.”

Fulfilling this query requires eight discrete computational stages: PDF extraction, OCR fallback, chunking, embedding generation, Qdrant vector retrieval, multi-turn agent reasoning, validation, and final report compilation. This workload requires between 45 seconds and two minutes.

The Black-Box Failure Mode

If the server emits zero telemetry until final completion, the user stares at a static spinner. After 10 seconds, users assume the application has frozen or crashed. Invariably, they reload the page, creating duplicate background jobs that double server cluster load!

We must deliver two distinct real-time feedback mechanisms:

1. Background Job Progress Streaming

Extracting text 10% → Generating embeddings 35% → Indexing Qdrant 60% → Reasoning 85% → Report Ready 100%

2. AI Text Token Streaming

User: Explain invoice 1042.
AI: This [invoice] [contains] [three] [critical] [financial] [risks]... (Incremental token emission)

02 Comparative Evaluation: Polling, Streaming, WebSockets vs. Webhooks

Selecting the correct communication primitive is vital to preventing architectural bloat and resource exhaustion:

Mechanism Directionality Transport Protocol Primary Use Case (Best Fit)
SSE (Server-Sent Events) Unidirectional (Server → Client) HTTP/1.1 & HTTP/2 Streaming LLM token streaming, conversational responses, and background job progress.
WebSocket Bidirectional (Full-Duplex) TCP WebSocket Protocol Real-time voice interactions, multiplayer collaboration, and live whiteboards.
Short/Long Polling Client-driven repeated query Standard discrete HTTP requests Basic status checks in legacy environments without streaming infrastructure.
Webhook Server-to-Server push Outbound HTTP POST Asynchronous event delivery to customer ERPs, CRM platforms, and payment systems.
Golden Engineering Axiom: Celery executes heavy computation; SSE delivers live progress to human browser sessions; and Webhooks notify automated remote servers. They are not competitors; in a mature system, all three operate harmoniously.

03 Enterprise Real-Time AI Architecture Blueprint

The end-to-end telemetry and event-driven architecture is structured across decoupled pipelines:

[React / TypeScript Client] │ ▲ │ (1) POST /jobs │ (5) SSE Stream: event: progress, percent: 45 ▼ │ [FastAPI Gateway] ─────────┘ │ ├────► (2) PostgreSQL: INSERT INTO ai_jobs (status='queued') │ ▼ (3) Dispatch Job Message [Redis Message Broker] │ ▼ (4) Pull Task (Prefetch=1) [Celery Background Worker] │ ├────► Execute Multi-Stage Pipeline (Extract → Chunk → Embed) │ ├────► (4a) Append Event → [Redis Stream / PostgreSQL Event Table] │ ├────► (4b) Update Job State → PostgreSQL (status='completed') │ ▼ (6) Trigger Outbound Webhook Delivery [Webhook Delivery Worker] │ ▼ (7) Signed HTTP POST (HMAC-SHA256) [Customer's ERP / Remote Server]

Three Non-Negotiable Data Retention Rules:

  1. PostgreSQL Job Table: Authoritative master for final business states and durable results.
  2. Event Stream (Redis Streams / Event Table): Ephemeral channel for transient progression updates.
  3. Webhook Delivery Log: Audit log capturing every delivery attempt, HTTP status code, and retry timestamp.

04 Polling Mechanics and Architectural Pitfalls

Polling is the simplest approach, wherein client JavaScript polls the server at periodic intervals:


// Naive polling implementation in frontend
async function pollJobStatus(jobId: string) {
  const timer = setInterval(async () => {
    try {
      const res = await fetch(`/v1/jobs/${jobId}`);
      const data = await res.json();
      console.log("Current progress:", data.progress);
      if (data.status === "completed" || data.status === "failed") {
        clearInterval(timer); // Terminate timer on completion
      }
    } catch (err) {
      console.error("Polling error:", err);
    }
  }, 2000); // Check every 2 seconds
}
Advantages of Polling

Trivial to implement; eliminates long-held server connections; gracefully recovers when client devices transition across networks.

Architectural Pitfalls

10,000 active clients querying every 2 seconds generate 5,000 redundant requests/sec; updates suffer 1–2s latency jitter.

05 Server-Sent Events (SSE) Protocol Mechanics

Server-Sent Events (SSE) is a W3C/WHATWG standard allowing a server to push continuous text event streams to web clients over a persistent standard HTTP connection.

Browser ───────── HTTP GET /v1/demo/stream ─────────> FastAPI Server Browser <─────── HTTP 200 OK (Content-Type: text/event-stream) ────── Browser <─────── event: progress data: {"percent": 25} ───────── Browser <─────── event: token data: {"text": "Financial "} ─────── Browser <─────── id: 104 event: completed data: {"status": "ok"}

SSE Wire Protocol Framing Specifications:

  • id: <identifier> — Monotonic event identifier. If severed, browsers pass this value in the Last-Event-ID header upon reconnecting.
  • event: <name> — Event type tag (e.g., token, progress, completed, error).
  • data: <payload> — Serialized event payload (typically JSON strings).
  • (Double Newline) — Mandatory boundary delimiter terminating the frame.
The Reverse Proxy Buffering Trap (X-Accel-Buffering: no)

Reverse proxies such as Nginx, Cloudflare, or AWS ALBs buffer downstream responses by default to conserve TCP sockets. If buffering remains enabled, model tokens will not stream progressively; instead, the proxy buffers the output for 30 seconds and flushes the entire response all at once! To disable proxy buffering, always set Cache-Control: no-cache and X-Accel-Buffering: no.

06 WebSocket Protocol and Bidirectional Channels

WebSockets upgrade an HTTP handshake into a persistent TCP socket (ws:// or wss://), facilitating full-duplex framing where both client and server transmit payloads simultaneously.

React Client ──── User Voice / Input ────> Server Node React Client <─── AI Speech Audio Stream ── Server Node React Client ──── Interrupt / Barge-in ───> Server Node React Client <─── Model State Updates ──── Server Node

Why is SSE Preferred over WebSockets for Conversational AI?

  • SSE operates seamlessly over standard HTTP/2 and HTTP/3 without proxy blocking or corporate firewall drops.
  • Browsers provide automatic reconnection and event tracking natively via EventSource.
  • Eliminates complex socket state clusters, distributed ping/pong heartbeats, and sticky session overhead.

07 Architectural Decision Framework: Which Primitive to Choose?

Apply the following engineering criteria to select the appropriate real-time transport:

• Conversational Chat & Text Inference: SSE or authenticated fetch() ReadableStream.
• Celery Background Ingestion Progress: SSE event stream (or simple polling for basic jobs).
• Bidirectional Voice/Audio AI or Live Canvases: WebSockets or WebRTC.
• Notifying Remote Enterprise Systems: Cryptographically signed Outbound Webhooks.

Never adopt WebSockets purely for novelty. Choose the simplest transport primitive capable of satisfying system reliability requirements.

08 Part B: Production Project Structure

To integrate real-time streaming and webhooks into our backend, we structure the workspace into clean modules:

byteion-realtime-ai/ ├── app/ │ ├── main.py # FastAPI application entrypoint & lifespan │ ├── api/ │ │ ├── answers.py # POST /v1/answers/stream (Token streaming) │ │ ├── jobs.py # GET /v1/jobs/{id}/events (Job progress SSE) │ │ └── webhooks.py # Inbound & outbound webhook endpoints │ ├── schemas/ │ │ └── events.py # Pydantic schemas (token, progress, error) │ ├── services/ │ │ ├── ai_stream.py # OpenAI Responses API streaming adapter │ │ └── webhook_service.py # HMAC-SHA256 signatures & delivery workers │ └── security/ │ ├── auth.py # JWT claims & tenant isolation dependencies │ └── ssrf_guard.py # Outbound webhook SSRF defenses └── requirements.txt

09 Building a Demo SSE Endpoint with FastAPI

We begin by implementing an offline demo endpoint to master SSE wire framing and async generators:


import asyncio
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI(title="Byteion Real-Time AI")

def sse_frame(event: str, data: dict, event_id: str | None = None) -> str:
    """Format payload according to W3C Server-Sent Events wire spec."""
    lines = []
    if event_id is not None:
        lines.append(f"id: {event_id}")
    lines.append(f"event: {event}")
    lines.append("data: " + json.dumps(data, ensure_ascii=False))
    return "\n".join(lines) + "\n\n"

@app.get("/v1/demo/stream")
async def demo_stream():
    async def event_generator():
        yield sse_frame("progress", {"stage": "starting", "percent": 0}, event_id="1")
        sample_tokens = ["AI", "Engineering", "demands", "resilient", "real-time", "systems."]
        for idx, token in enumerate(sample_tokens, start=2):
            await asyncio.sleep(0.12) # Simulate model latency
            yield sse_frame("token", {"text": token + " "}, event_id=str(idx))
        yield sse_frame("completed", {"status": "success"}, event_id="8")

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
            "Connection": "keep-alive",
        },
    )

Execute curl -N with unbuffered terminal output to observe live stream delivery:


curl -N http://127.0.0.1:8000/v1/demo/stream

10 Live AI Token Streaming with OpenAI Responses API

We now replace simulated delays with genuine streaming inference using OpenAI Responses API:


# app/services/ai_stream.py
import os
import json
import logging
from openai import AsyncOpenAI

logger = logging.getLogger(__name__)
client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY", "mock-key"))

def sse_frame(event: str, data: dict, event_id: str | None = None) -> str:
    lines = []
    if event_id is not None:
        lines.append(f"id: {event_id}")
    lines.append(f"event: {event}")
    lines.append("data: " + json.dumps(data, ensure_ascii=False))
    return "\n".join(lines) + "\n\n"

async def stream_ai_answer(prompt: str):
    sequence = 0
    try:
        # Request semantic streaming from OpenAI Responses API
        stream = await client.responses.create(
            model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
            input=prompt,
            stream=True,
        )

        async for event in stream:
            # Output text delta emission
            if event.type == "response.output_text.delta":
                sequence += 1
                yield sse_frame("token", {"text": event.delta}, str(sequence))
            
            # Model output complete
            elif event.type == "response.completed":
                sequence += 1
                yield sse_frame("completed", {"response_id": event.response.id, "status": "completed"}, str(sequence))
            
            # Provider stream error
            elif event.type == "error":
                sequence += 1
                yield sse_frame("error", {"code": "provider_error", "message": "Stream interrupted by provider."}, str(sequence))
                return

    except Exception as exc:
        logger.exception("AI streaming invocation failed")
        sequence += 1
        yield sse_frame("error", {"code": "server_error", "message": "Internal generation failure."}, str(sequence))

Next, we expose the streaming router in app/api/answers.py:


# app/api/answers.py
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app.services.ai_stream import stream_ai_answer

router = APIRouter(prefix="/v1/answers", tags=["answers"])

class AnswerRequest(BaseModel):
    prompt: str = Field(min_length=1, max_length=4000)

@router.post("/stream")
async def answer_stream(payload: AnswerRequest):
    return StreamingResponse(
        stream_ai_answer(payload.prompt),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
            "Connection": "keep-alive",
        },
    )

11 Part C: Why Use fetch() Streaming Over Native EventSource?

Browser native EventSource issues only HTTP GET requests without custom header injection. In enterprise software, however, architectural security requires:

  • POST Method: Transmitting extensive prompt payloads within the HTTP request body.
  • Authorization Headers: Providing Authorization: Bearer <jwt-token> without exposing tokens in query strings or server access logs.

Consequently, streaming via fetch() combined with the ReadableStream reader is the industry standard for secure frontend architectures.

12 Building a Chunk-Boundary Safe SSE Parser (src/lib/sse.ts)

TCP chunk boundaries rarely align with SSE message boundaries. A single network chunk may contain fractional frames, or multiple complete messages bundled together.

We implement a robust TypeScript parser resolving framing boundaries, event types, and keep-alive comments:


// src/lib/sse.ts
export type ParsedSSEEvent = {
  event: string;
  data: string;
  id?: string;
};

export function parseSSEFrame(frame: string): ParsedSSEEvent | null {
  let event = "message";
  let id: string | undefined;
  const dataLines: string[] = [];

  for (const line of frame.split("\n")) {
    if (line.startsWith(":")) {
      // SSE comment / heartbeat ping
      continue;
    }
    const separator = line.indexOf(":");
    const field = separator === -1 ? line : line.slice(0, separator);
    let value = separator === -1 ? "" : line.slice(separator + 1);

    if (value.startsWith(" ")) {
      value = value.slice(1);
    }

    if (field === "event") {
      event = value;
    } else if (field === "data") {
      dataLines.push(value);
    } else if (field === "id") {
      id = value;
    }
  }

  if (dataLines.length === 0) return null;

  return {
    event,
    data: dataLines.join("\n"),
    id,
  };
}

13 Complete React/TypeScript AI Chat Component

We assemble a production-grade React client handling abort signals, token accumulation, and terminal error states:


// src/components/AIAnswer.tsx
import React, { useState, useRef } from "react";
import { parseSSEFrame } from "../lib/sse";

export function AIAnswer({ authToken }: { authToken: string }) {
  const [prompt, setPrompt] = useState("");
  const [answer, setAnswer] = useState("");
  const [status, setStatus] = useState<"idle" | "streaming" | "completed" | "failed" | "cancelled">("idle");
  const controllerRef = useRef<AbortController | null>(null);

  async function handleGenerate() {
    controllerRef.current?.abort();
    const controller = new AbortController();
    controllerRef.current = controller;

    setAnswer("");
    setStatus("streaming");

    try {
      const response = await fetch("/v1/answers/stream", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${authToken}`,
        },
        body: JSON.stringify({ prompt }),
        signal: controller.signal,
      });

      if (!response.ok || !response.body) {
        throw new Error(`Streaming failed with status: ${response.status}`);
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";

      while (true) {
        const { value, done } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        buffer = buffer.replace(/\r\n/g, "\n");

        let boundary: number;
        while ((boundary = buffer.indexOf("\n\n")) !== -1) {
          const frame = buffer.slice(0, boundary);
          buffer = buffer.slice(boundary + 2);

          const parsed = parseSSEFrame(frame);
          if (!parsed) continue;

          const data = JSON.parse(parsed.data);
          if (parsed.event === "token" && data.text) {
            setAnswer((prev) => prev + data.text);
          } else if (parsed.event === "completed") {
            setStatus("completed");
          } else if (parsed.event === "error") {
            setStatus("failed");
            throw new Error(data.message || "Model error");
          }
        }
      }
    } catch (err: any) {
      if (controller.signal.aborted) {
        setStatus("cancelled");
      } else {
        setStatus("failed");
      }
    }
  }

  return (
    <div className="rounded-xl border border-border p-4 bg-card">
      <textarea
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        className="w-full rounded-lg border border-border p-2 text-xs bg-background"
        placeholder="Ask a question..."
      />
      <div className="flex gap-2 mt-2">
        <button
          onClick={handleGenerate}
          disabled={status === "streaming" || !prompt.trim()}
          className="bg-cyan-600 text-white px-3 py-1.5 rounded-lg text-xs font-bold"
        >
          Generate
        </button>
        <button
          onClick={() => controllerRef.current?.abort()}
          disabled={status !== "streaming"}
          className="border border-border text-foreground px-3 py-1.5 rounded-lg text-xs"
        >
          Stop
        </button>
      </div>
      <div className="mt-3 text-xs text-muted-foreground">Status: {status}</div>
      <div className="mt-2 p-3 bg-muted/30 rounded-lg text-xs font-mono whitespace-pre-wrap">{answer || "..."}</div>
    </div>
  );
}

14 Part D: Short Streams vs. Long-Running Background Workflows

In enterprise AI engineering, not all streams share the same lifecycle. We separate streaming into two architectural categories:

Short Conversational Stream

POST /v1/answers/stream — The client initiates an HTTP request; FastAPI proxies token frames directly from the foundation model.

Browser Close → Abort Signal → Model Cancel

The compute lifecycle is strictly bound to the client HTTP socket connection.

Durable Long-Running Job

POST /v1/document-jobs — Ingesting a 200-page PDF. The client receives 202 Accepted instantly.

Browser Close → Celery Worker Continues → DB Persists

Worker execution survives browser disconnection. Users reconnect later to inspect full telemetry.

15 PostgreSQL Relational Schemas for Jobs and Event Logs

To support event replay, order preservation, and auditing, we define dedicated relational tables in PostgreSQL:


-- Authoritative business job record
CREATE TABLE ai_jobs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    created_by UUID NOT NULL,
    status VARCHAR(30) NOT NULL DEFAULT 'queued', -- 'queued', 'running', 'completed', 'failed'
    stage VARCHAR(100),
    progress SMALLINT NOT NULL DEFAULT 0,
    result JSONB,
    error_code VARCHAR(100),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Durable incremental event log supporting replay
CREATE TABLE ai_job_events (
    job_id UUID NOT NULL REFERENCES ai_jobs(id) ON DELETE CASCADE,
    event_sequence BIGINT NOT NULL,
    event_type VARCHAR(40) NOT NULL, -- 'progress', 'token', 'tool_status', 'completed'
    payload JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (job_id, event_sequence)
);

CREATE INDEX idx_ai_job_events_replay 
ON ai_job_events (job_id, event_sequence ASC);

16 Redis Pub/Sub vs. Redis Streams: Durability and Replay

While Redis acts as a broker for Celery, delivering live events to browser clients requires choosing between two distinct primitives:

Dimension Redis Pub/Sub Redis Streams (XADD / XREAD)
Message Durability Ephemeral (Fire-and-Forget memory buffer) Durable (Appended to persistent memory log)
Reconnection Replay Impossible; messages missed during disconnect are lost Native; replay from any given entry ID or offset
Consumer Groups Unsupported (All active clients receive broadcast) Supported (Distributed acknowledgment and fan-out)
Optimal Fit Transient presence, typing indicators, ephemeral alerts Durable AI job progression, telemetry pipelines, event sourcing

17 Structured Event Contracts (Pydantic Schemas)

To eliminate contract drift between frontend and backend services, we define formal Pydantic models for event payloads:


# app/schemas/events.py
from pydantic import BaseModel, Field

class ProgressEventPayload(BaseModel):
    job_id: str
    stage: str
    percent: int = Field(ge=0, le=100)
    message: str | None = None

class TokenEventPayload(BaseModel):
    run_id: str
    text: str

class CompletedEventPayload(BaseModel):
    job_id: str
    status: str = "completed"
    result_url: str

class ErrorEventPayload(BaseModel):
    job_id: str
    code: str
    message: str
    retryable: bool = False

18 Reliable Job Progress and the Completion Invariant

A pervasive anti-pattern in real-time engineering is broadcasting the completed event prior to committing final artifacts into the database:

The Phantom 404 Race Condition

A worker finishes model synthesis and immediately broadcasts event: completed over Redis. The browser receives the frame and fetches GET /v1/jobs/{id}/result. However, the database transaction is still in-flight! The client receives an unexpected 404 Not Found or partial payload, triggering client-side crashes!

The Non-Negotiable Completion Invariant:
1. Atomically commit status = 'completed', artifacts, and token metrics into PostgreSQL.
2. Flush any heavy output files directly into durable object storage (S3/MinIO).
3. Only AFTER database commit acknowledgement is verified may the worker emit the final completed event frame.

19 Part E: Event Stream Security & Multi-Tenant Isolation

An event stream is an authoritative API endpoint. Without strict tenant verification, malicious actors guessing job UUIDs can eavesdrop on competitor telemetry:

# app/api/jobs.py security verification pattern: @router.get("/v1/jobs/{job_id}/events") async def stream_job_events( job_id: UUID, principal: AuthenticatedPrincipal = Depends(get_current_principal), job_repo: JobRepository = Depends(get_job_repository), ): job = await job_repo.get_by_id(job_id) if job is None or job.tenant_id != principal.tenant_id: # Return 404 to prevent resource existence disclosure raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return StreamingResponse( event_generator(job_id, principal.tenant_id), media_type="text/event-stream" )
  • Zero IDOR Tolerance: Never accept tenant_id from query parameters; source tenant context exclusively from validated JWT claims.
  • No Sensitive Secrets in SSE: Never emit private reasoning traces, system prompts, or internal tool secrets across client streams; sanitize all telemetry.

20 Part F: What is a Webhook?

While SSE streams real-time frames directly into an active browser, a Webhook is an asynchronous server-to-server messaging pattern where our backend issues an HTTP POST push to a customer-configured remote server upon a business event.

POST https://customer.example.com/integrations/byteion Content-Type: application/json X-Byteion-Event-Id: evt_9021a8 X-Byteion-Timestamp: 1792072800 X-Byteion-Signature: t=1792072800,v1=a4f89d... { "id": "evt_9021a8", "type": "document.analysis.completed", "created_at": "2026-10-15T08:00:00Z", "data": { "job_id": "job_1042", "status": "completed", "result_url": "https://api.byteion.com/v1/jobs/job_1042/result" } }

The objective is not interactive rendering, but notifying remote corporate applications (like SAP, Salesforce, or custom ERPs) that heavy AI analysis has concluded.

21 Inbound vs. Outbound Webhooks

Enterprise platforms handle webhooks across two distinct ingress and egress vectors:

Inbound Webhooks

Third-party providers (e.g., Stripe, Twilio) push state transitions into Byteion:

Stripe → Byteion Gateway → Verify Sig → DB

Responsibilities: Verify signatures, enforce replay windows, acknowledge with 200 OK in < 200ms.

Outbound Webhooks

Byteion workers dispatch cryptographically signed notifications to customer servers:

Byteion Worker → Sign Payload → Customer ERP

Responsibilities: Generate HMAC signatures, handle exponential retries, and strictly prevent SSRF.

22 Reliable Webhook Delivery & Dead-Letter Queues (DLQ)

What occurs when a customer’s web server experiences downtime, throws 500 Internal Errors, or drops network connections during delivery?

We architect outbound deliveries as dedicated, persistent state records in PostgreSQL:


CREATE TABLE webhook_deliveries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    event_id UUID NOT NULL,
    endpoint_url TEXT NOT NULL,
    attempt_count INTEGER NOT NULL DEFAULT 0,
    status VARCHAR(30) NOT NULL DEFAULT 'pending', -- 'pending', 'succeeded', 'retrying', 'dead_letter'
    next_attempt_at TIMESTAMPTZ,
    last_http_status INTEGER,
    last_error TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_webhook_deliveries_pending 
ON webhook_deliveries (status, next_attempt_at) 
WHERE status IN ('pending', 'retrying');
Retry and Dead-Letter Policies:
1. Transient failures (5xx, 429, Socket Timeouts) retry across exponential intervals up to 5 attempts (30s, 2m, 10m, 1h, 6h).
2. Permanent client faults (400, 404, 401) abort retries immediately to preserve compute resources.
3. Exhausted attempts transition to dead_letter for manual review and replay via admin tooling.

23 Webhook Idempotency and Deduplication Architecture

Because network partitions induce duplicate webhook deliveries (at-least-once semantics), receivers must implement idempotent deduplication gates:

[Receive Webhook POST] │ ▼ (1) Verify HMAC-SHA256 Signature & Timestamp Window [Check Deduplication Table] │ ├──► Event ID Already Processed? → Return HTTP 200 OK Immediately │ ▼ (2) INSERT INTO processed_events (event_id, processed_at) │ ▼ (3) Enqueue Internal Work to Background Worker │ ▼ (4) Return HTTP 200/202 to Sender in < 200ms

24 Cryptographic Signatures with HMAC-SHA256

To neutralize tampering and replay attacks, outbound webhooks must be cryptographically signed using shared secrets and canonical timestamps:


# app/security/webhook_signatures.py
import hmac
import hashlib
import time

def sign_webhook_payload(secret: str, raw_body: bytes) -> tuple[int, str]:
    """
    Constructs an HMAC-SHA256 signature over timestamp and raw body bytes.
    Canonical format: timestamp.raw_body
    """
    timestamp = int(time.time())
    signed_payload = str(timestamp).encode("utf-8") + b"." + raw_body
    digest = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
    return timestamp, digest

def verify_webhook_signature(
    secret: str,
    raw_body: bytes,
    signature_header: str,
    timestamp_header: int,
    tolerance_seconds: int = 300,
) -> bool:
    """
    Verifies signature using constant-time comparison and enforces timestamp tolerance window.
    """
    now = int(time.time())
    if abs(now - timestamp_header) > tolerance_seconds:
        return False # Mitigate replay attacks outside 5-minute window

    signed_payload = str(timestamp_header).encode("utf-8") + b"." + raw_body
    expected_digest = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
    
    # Constant-time comparison prevents timing attacks
    return hmac.compare_digest(expected_digest, signature_header)

25 Part G: Customer Webhook URLs and Server-Side Request Forgery (SSRF) Defense

Permitting customers to provide arbitrary outbound webhook destination URLs introduces a critical vector for Server-Side Request Forgery (SSRF):

Malicious Ingress Destinations: http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS Cloud Metadata) http://127.0.0.1:8000/internal-admin/ http://10.0.0.42:5432/ (Internal VPC PostgreSQL)

Blindly issuing HTTP requests against customer endpoints allows adversaries to extract IAM instance credentials and pivot into private subnets.

Five Mandatory SSRF Countermeasures (app/security/ssrf_guard.py):
1. Strict HTTPS Enforcement: Restrict scheme strictly to https://; reject plaintext HTTP, file, or gopher schemes.
2. DNS Pre-Resolution & CIDR Filtering: Resolve hostnames and inspect resolved IP addresses against private and link-local ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, and IPv6 loopback ::1).
3. DNS Rebinding Defense: Connect directly to the verified resolved IP address rather than re-resolving during transmission.
4. Disable HTTP Redirects: Configure HTTP clients with follow_redirects=False to prevent public domains from pivoting to private hosts.
5. Strict Socket Timeouts: Enforce 5-second socket ceilings to prevent slow-loris connection pool exhaustion.

26 Part H: Streaming Autonomous Agent Events with OpenAI Agents SDK

As established in Lessons 21 and 24, an autonomous agent does far more than emit static text deltas: it dispatches tools, delegates to specialist subagents, and requests human sign-off.

Leveraging OpenAI Agents SDK’s Runner.run_streamed(), we capture and stream both raw tokens and semantic execution stages:


# app/services/agent_stream.py
from agents import Agent, Runner
from openai.types.responses import ResponseTextDeltaEvent

async def stream_agent_execution(prompt: str):
    agent = Agent(
        name="Enterprise Financial Auditor",
        instructions="Analyze financial risks and use compliance tools when required.",
    )

    result = Runner.run_streamed(agent, input=prompt)

    async for event in result.stream_events():
        # Case 1: Live raw text delta
        if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
            yield f"event: token\ndata: {json.dumps({'text': event.data.delta})}\n\n"
        
        # Case 2: Multi-agent handoff event
        elif event.type == "agent_updated_stream_event":
            yield f"event: tool_status\ndata: {json.dumps({'stage': f'Switched agent to: {event.new_agent.name}'})}\n\n"

    if result.is_complete:
        yield f"event: completed\ndata: {json.dumps({'status': 'completed'})}\n\n"

27 Part I: Automated Testing Strategy (SSE, Progress & Webhooks)

Real-time pipelines possess higher operational complexity than stateless CRUD endpoints. We structure automated testing across three core domains:

1. SSE Framing and Chunk Boundary Verification

Assert that TCP buffer fragmentation across partial frames parses without state corruption, and verify the presence of X-Accel-Buffering: no headers.

2. Progress Reconnection and History Replay

Assert that severed clients passing Last-Event-ID: 12 resume reading from frame 13 onward, and ensure unauthorized cross-tenant requests return 404.

3. Webhook Cryptographic and SSRF Defense

Verify rejection of tampered HMAC signatures, enforce 5-minute replay windows, assert deduplication idempotency, and verify immediate rejection of 169.254.169.254.

28 Observability and Golden Metrics

Production monitoring requires continuous instrumentation of four core real-time telemetry signals:

Telemetry Metric Operational Significance
TTFT (Time to First Token) Measures perceived UX latency from request dispatch until the first token renders (Target: < 1.5s).
Stream Disconnect Rate Tracks the ratio of sockets severed before receiving terminal frames (signals network instability or prompt cancellation).
Event Replay Ratio Monitors reconnection frequency utilizing Last-Event-ID headers across transient cellular connections.
Webhook Delivery Success Rate Calculates the delivery ratio against customer enterprise endpoints (Target: > 99.9% SLA).

29 Production Deployment Checklist (12 Core Mandates)

  1. 1. Enforce JWT authentication and strict tenant-scoped authorization.
  2. 2. Inject X-Accel-Buffering: no headers to disable reverse proxy buffers.
  3. 3. Propagate client abort signals to cancel upstream foundation model runs.
  4. 4. Version all streaming event contracts using typed Pydantic models.
  5. 5. Commit final artifacts to PostgreSQL prior to emitting completed.
  6. 6. Support Last-Event-ID headers to replay history upon reconnect.
  7. 7. Configure maxlen trimming and retention policies on Redis Streams.
  8. 8. Architect exponential backoffs and dead-letter queues for webhooks.
  9. 9. Sign webhook payloads via HMAC-SHA256 with 5-minute replay tolerance.
  10. 10. Strictly neutralize SSRF by validating resolved IPs against private CIDRs.
  11. 11. Support dual-key verification during webhook secret rotation cycles.
  12. 12. Sanitize structured logs to exclude bearer tokens, secrets, and raw prompts.

30 Hands-On Assignment: Real-Time AI Document Analyzer

Integrate Lesson 28’s Celery ingestion pipeline with this lesson’s real-time streaming and webhook delivery architecture:

1. POST /v1/document-jobs: - Accept PDF → Verify JWT → Insert PostgreSQL job → Return 202 Accepted. 2. GET /v1/document-jobs/{id}/events: - Verify tenant permissions → Stream SSE progress frames (10% → 50% → 100%). - If client reconnects with Last-Event-ID, replay missing frames from DB. 3. Outbound Webhook Delivery: - When status becomes 'completed', dispatch signed HTTP POST to customer URL. - Guard destination URL against SSRF (127.0.0.1 and 169.254.169.254 blocked).

Real-Time Streaming & Security Studio

Experiment with token streaming, Redis Streams replay, and SSRF-defended webhook signatures hands-on

// Client UI will progressively render here...
// Raw SSE wire frames will be logged here...

32 Knowledge Check: Test Your Understanding

Assess your mastery of real-time streaming, event architectures, and webhook security:

1. Do Server-Sent Events (SSE) and Celery fulfill the same operational purpose in AI backends?

Rationale: SSE is an HTTP event transport; Celery is an asynchronous distributed compute orchestrator.

2. Can historic progress events missed during client disconnection be replayed from Redis Pub/Sub?

Rationale: Redis Pub/Sub is ephemeral fire-and-forget; messages published without active subscribers are dropped.

3. At what precise moment must the 'completed' event be emitted during background job streaming?

Rationale: Emitting completed prior to database commit causes phantom 404 race conditions when clients fetch results.

4. Why is fetch() streaming preferred over browser EventSource for authenticated POST requests?

Rationale: Native EventSource only supports GET and prevents injecting custom Bearer token headers.

5. How do webhook receivers effectively mitigate duplicate event processing?

Rationale: Deduplication tables identify previously acknowledged IDs and return 200 OK without re-running actions.

6. Why must webhook signature verification operate on the raw request body bytes?

Rationale: HMAC signatures are calculated over exact canonical byte sequences; minor whitespace changes invalidate digests.

7. Does including the 'id' field in an SSE frame automatically guarantee missing event replay?

Rationale: Browsers pass the last received identifier; re-reading and dispatching unacknowledged frames requires custom server logic.

8. What architectural defense is mandatory when dispatching webhooks to customer-configured URLs?

Rationale: Resolving hosts and blocking private CIDRs prevents Server-Side Request Forgery against cloud metadata APIs.

9. Which response header prevents Nginx reverse proxies from buffering SSE token streams?

Rationale: The X-Accel-Buffering header instructs Nginx to immediately flush downstream frames to the socket.

10. What information is safe to broadcast in client-facing real-time agent event streams?

Rationale: Telemetry streams must sanitize internal reasoning details and emit only authorized business stage indicators.

33 Lesson Summary & Next Steps

In this lesson, we mastered how to deliver real-time feedback and event notifications across browser clients and enterprise remote servers.

1. Transport vs. Persistence Separation: SSE and WebSockets are delivery pipes; authoritative job state resides permanently in PostgreSQL.
2. Authenticated Frontend Streaming: Bypassing EventSource limitations by pairing fetch() ReadableStream with custom Bearer tokens.
3. Enterprise Webhook Defense: Pairing HMAC-SHA256 signatures, timestamp tolerance, deduplication, and rigorous SSRF private CIDR filters.
Next Lesson

Lesson 16.30: Building Production AI Interfaces

In the upcoming lesson, we will convert streaming tokens, tool indicators, and real-time event frames into a polished, responsive, and robust enterprise AI chat interface utilizing React, TypeScript, and Tailwind CSS.

Start Lesson 16.30