---
title: "Connection: GCP LB Timeout → REST Polling → Job Table Pattern"
description: "The chain from GCP infrastructure constraint to application architecture — why the 30s LB timeout forces a specific code pattern"
connects:
  - "architecture/gcp-deployment-lb-timeout"
  - "tech-patterns/redis-celery-worker-queue"
  - "tech-patterns/fastapi-python-docker"
created: 2026-04-27
updated: 2026-04-27
---

# Connection: GCP LB Timeout → REST Polling → Job Table Pattern

## The Connection

GCP load balancers terminate TCP connections after 30 seconds of inactivity. This single infrastructure constraint forces a specific application architecture pattern: job tables, HTTP polling, and eventually Celery for heavy workloads. The constraint propagates from infrastructure all the way to frontend code.

## Key Insight

**The 30s timeout isn't a WebSocket-specific problem — it affects any long-running synchronous request.** A `/api/analyze` endpoint that takes 45 seconds will also be killed. The fix isn't just replacing WebSockets; it's making the entire async workflow non-blocking within 30 seconds.

## The Constraint Chain

```
GCP Load Balancer: 30s TCP inactivity timeout
    ↓ forces
No WebSockets (connection held open → killed at 30s)
    ↓ forces
No long synchronous API calls > 30s
    ↓ forces
Job table pattern: POST creates job → GET polls status
    ↓ for heavy parallelism:
Redis + Celery for worker queue management
```

## Minimum Implementation (no Celery needed)

```python
# POST /api/jobs → returns immediately with job_id
@router.post("/jobs")
async def create_job(request: JobRequest, db: AsyncSession = Depends(get_db)):
    job = Job(status="pending", ...)
    db.add(job)
    await db.commit()
    asyncio.create_task(run_job(job.id))   # fire and forget
    return {"job_id": job.id}

# GET /api/jobs/{id} → returns current status
@router.get("/jobs/{job_id}")
async def get_job(job_id: str, db: AsyncSession = Depends(get_db)):
    job = await db.get(Job, job_id)
    return {"status": job.status, "result": job.result}
```

```ts
// Frontend: poll every 2s until done
const poll = async (jobId: string) => {
  while (true) {
    const r = await fetch(`${API}/api/jobs/${jobId}`);
    const { status, result } = await r.json();
    if (status === "done") return result;
    if (status === "error") throw new Error(result);
    await new Promise(r => setTimeout(r, 2000));
  }
};
```

## When to Add Celery

The simple `asyncio.create_task()` pattern breaks when:
- Multiple AI agents run in parallel and saturate event loop
- Tasks need retry logic on failure
- Need to see queue depth / worker utilization
- Jobs run for >5 minutes

For those cases: see [[wiki/tech-patterns/redis-celery-worker-queue|redis-celery-worker-queue]].

## Applies Also to optical-dev

Apache + corporate LB timeout is 30–60s. Same pattern required on optical-dev, not just GCP.

## Projects Using This Pattern

- Mod Comms: switched from WebSocket after production incident (2026-03-18)
- Enterprise Nexus: HTTP polling from day 1
- Video Accessibility: Celery for heavy FFmpeg + AI pipeline

## Related

- [[wiki/architecture/gcp-deployment-lb-timeout|gcp-deployment-lb-timeout]] — full implementation
- [[wiki/tech-patterns/redis-celery-worker-queue|redis-celery-worker-queue]] — when Celery is needed
- [[wiki/architecture/adr-log|adr-log]] — ADR-001 documents this decision
- [[wiki/client-knowledge/barclays|barclays]] — Mod Comms incident
