python 112 lines · 3 tabs

Long-Polling a Background Job's Status with a Shared Result Registry in FastAPI

Shared by codesnips Sep 2026
3 tabs
import asyncio
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, Optional


@dataclass
class JobState:
    job_id: str
    status: str = "pending"
    result: Optional[Any] = None
    error: Optional[str] = None
    event: asyncio.Event = field(default_factory=asyncio.Event)


class JobRegistry:
    def __init__(self):
        self._jobs: Dict[str, JobState] = {}
        self._lock = asyncio.Lock()

    async def create(self) -> JobState:
        state = JobState(job_id=uuid.uuid4().hex)
        async with self._lock:
            self._jobs[state.job_id] = state
        return state

    def get(self, job_id: str) -> Optional[JobState]:
        return self._jobs.get(job_id)

    async def complete(self, job_id: str, result: Any) -> None:
        state = self._jobs.get(job_id)
        if state is None:
            return
        state.status = "succeeded"
        state.result = result
        state.event.set()

    async def fail(self, job_id: str, error: str) -> None:
        state = self._jobs.get(job_id)
        if state is None:
            return
        state.status = "failed"
        state.error = error
        state.event.set()

    async def wait(self, job_id: str, timeout: float) -> Optional[JobState]:
        state = self._jobs.get(job_id)
        if state is None:
            return None
        if state.event.is_set():
            return state
        try:
            await asyncio.wait_for(state.event.wait(), timeout=timeout)
        except asyncio.TimeoutError:
            pass  # still pending; caller re-polls
        return state
3 files · python Explain with highlit

This snippet shows how to implement HTTP long-polling for background job status in an async FastAPI service, backed by an in-process registry that decouples the workers producing results from the clients waiting on them. Long-polling avoids the wasteful tight-loop retries of naive client polling: instead of returning 202 immediately and forcing the client to hammer the endpoint, the request parks on the server until the job finishes or a timeout elapses, then returns.

The JobRegistry tab is the core primitive. Each job gets a JobState holding its status, an eventual result, and an asyncio.Event used as a completion signal. create registers a pending job; complete and fail set the terminal state and call event.set() to wake every coroutine currently awaiting that job. The key method is wait, which uses asyncio.wait_for(state.event.wait(), timeout) so a single event can fan out to many waiters — any number of concurrent long-poll requests can await the same job without polling shared state in a loop. On TimeoutError it returns the still-pending snapshot rather than raising, letting the caller re-poll cleanly.

The worker tab simulates the actual job: it is scheduled with asyncio.create_task and, on completion, funnels its outcome back through registry.complete or registry.fail. Because the registry is the only shared surface, the worker never needs to know who is waiting or how many clients there are.

The routes tab wires it into HTTP. POST /jobs creates a job, spawns its worker, and returns the job_id. GET /jobs/{job_id} is the long-poll endpoint: it awaits registry.wait up to a bounded timeout, then returns 200 with the result if finished or 202 if still pending, so the client simply reissues the request. A Retry-After hint on the 202 guides well-behaved clients.

The trade-off is that this registry lives in a single process's event loop, so it suits one ASGI worker or must be replaced by Redis pub/sub for horizontal scaling. Bounding the timeout below typical proxy idle limits (often 60s) is essential to avoid dropped connections, and cleaning up finished JobState entries prevents unbounded memory growth.


Related snips

Share this code

Here's the card — post it anywhere.

Long-Polling a Background Job's Status with a Shared Result Registry in FastAPI — share card
Link copied