Skip to main content

Overview

Server-Sent Events (SSE) provide real-time progress updates for running audits. SSE replaces polling and reduces server load while improving responsiveness.
SSE requires standard JWT Bearer authentication via the Authorization header.

Stream Audit Progress

GET /api/v1/sse/audits/{audit_id}/progress Establishes an SSE connection for real-time audit progress updates.
const eventSource = new EventSource(
  'https://api.latentgeo.com/api/v1/sse/audits/123/progress',
  {
    headers: {
      'Authorization': 'Bearer YOUR_TOKEN'
    }
  }
);

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Progress:', data.progress + '%');
  console.log('Status:', data.status);
  
  if (data.status === 'completed' || data.status === 'failed') {
    eventSource.close();
  }
};

eventSource.onerror = (error) => {
  console.error('SSE error:', error);
  eventSource.close();
};
event: message
data: {"audit_id": 123, "status": "running", "progress": 25, "message": "Analyzing pages..."}

event: message
data: {"audit_id": 123, "status": "running", "progress": 50, "message": "Running competitor analysis..."}

event: message  
data: {"audit_id": 123, "status": "running", "progress": 75, "message": "Generating report..."}

event: message
data: {"audit_id": 123, "status": "completed", "progress": 100, "geo_score": 78.5}

Event Data Structure

Each SSE message contains a JSON payload with the following fields:
audit_id
integer
Audit ID
status
string
Current status: pending, running, completed, failed
progress
integer
Progress percentage (0-100)
message
string
Human-readable progress message
geo_score
number
GEO score (present when available)
total_pages
integer
Number of pages analyzed (when available)
error
string
Error message (only present on failure)

Stream Behavior

Initial Event

The stream immediately sends the current audit state upon connection.

Progress Updates

Progress updates are sent in real-time:
  • Redis mode (production): Updates pushed via Redis pub/sub
  • Database mode (fallback): Polls database every 10 seconds
  • Heartbeat: Sent every 30 seconds if no updates

Terminal Events

The stream automatically closes when the audit reaches a terminal state:
  • completed: Audit finished successfully
  • failed: Audit failed with an error

Timeouts

  • Max duration: 3600 seconds (1 hour)
  • Reconnect retry: 5000ms (client should retry with this interval)

Configuration

SSE behavior is controlled by environment variables:
VariableDefaultDescription
SSE_SOURCEredisData source (redis or db)
SSE_MAX_DURATION3600Max stream duration (seconds)
SSE_HEARTBEAT_SECONDS30Heartbeat interval (seconds)
SSE_FALLBACK_DB_INTERVAL_SECONDS10DB polling interval (seconds)
SSE_RETRY_MS5000Client retry interval (milliseconds)

Error Handling

Stream Timeout

{
  "error": "Stream timeout"
}
Sent when the stream reaches max duration. Client should reconnect or switch to polling.

Internal Error

{
  "error": "Internal server error"
}
Sent on unexpected errors. Client should implement exponential backoff.

Best Practices

Always close the EventSource when you receive a terminal status (completed/failed) to free up server resources.

Connection Management

  1. Automatic reconnection: Browsers automatically reconnect on disconnect
  2. Manual close: Call eventSource.close() on terminal events
  3. Error handling: Implement exponential backoff on repeated errors

Fallback Strategy

If SSE is not supported or fails:
  1. Detect SSE support: Check typeof EventSource !== 'undefined'
  2. Fallback to polling: Use GET /api/v1/audits/{audit_id}/status
  3. Poll interval: 5-10 seconds for running audits

Example: React Hook

import { useEffect, useState } from 'react';

interface AuditProgress {
  audit_id: number;
  status: string;
  progress: number;
  message?: string;
  geo_score?: number;
}

export function useAuditProgress(auditId: number, token: string) {
  const [progress, setProgress] = useState<AuditProgress | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const eventSource = new EventSource(
      `https://api.latentgeo.com/api/v1/sse/audits/${auditId}/progress`,
      { headers: { Authorization: `Bearer ${token}` } }
    );

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setProgress(data);
      
      if (data.status === 'completed' || data.status === 'failed') {
        eventSource.close();
      }
    };

    eventSource.onerror = (err) => {
      setError('Connection error');
      eventSource.close();
    };

    return () => {
      eventSource.close();
    };
  }, [auditId, token]);

  return { progress, error };
}

Error Codes

401
error
Missing or invalid authentication token
403
error
Cross-user access denied
404
error
Audit not found

Build docs developers (and LLMs) love