Skip to main content

Apply backpressure before a queue grows without bounds

2026-11-11 16:36:27.676537+00:00
Apply backpressure before a queue grows without bounds

If producers create work faster than workers finish it, a queue can become an ever-growing waiting room. Backpressure gives the producer an explicit admission decision before it accepts more work.

Cooperating producers check capacity while holding the shared admission-row lock.

Make admission atomic where the work is stored

This Django fragment assumes a toy Workspace row and a Job table. Every producer for the workspace must use this service for the example limit to hold.

from django.db import transaction

def accept_job(workspace_id, payload):
    with transaction.atomic():
        Workspace.objects.select_for_update().get(pk=workspace_id)
        outstanding = Job.objects.filter(
            workspace_id=workspace_id,
            state__in=["pending", "running"],
        ).count()
        if outstanding >= 100:
            return None
        return Job.objects.create(
            workspace_id=workspace_id,
            state="pending",
            payload=payload,
        )

The value 100 is an example backlog policy. It is not a claim about how much work a particular server can handle. The transaction locks the shared admission row so cooperating producers do not all observe the same count and over-admit concurrently.

This design stores accepted work durably in the application database. A separate dispatcher can enqueue pending IDs. The browser should receive a job ID only after acceptance is committed, and a clear capacity response when no job was accepted.

Count the work that still consumes capacity

Define whether retrying, delayed or exporting jobs belong in the outstanding set. If a state is omitted while it still consumes scarce resources, the nominal limit can be bypassed. Cancellation and terminal transitions need to update the same authoritative job lifecycle.

A broker-depth check alone is racy when several producers check and enqueue independently. It can still be a useful operational signal, but it should not be described as a strict distributed admission bound without coordination.

The database query here can become expensive at scale. Index the intended filter and measure it; a counter or reservation ledger may be appropriate when a measured bottleneck justifies the extra consistency work.

Test the boundary with 99 and 100 synthetic outstanding jobs, then exercise concurrent admissions on PostgreSQL. Confirm a rejected request does not leave an orphan job or trigger a provider action.

Queue age and completion rate help an operator understand pressure. A flat queue count can hide jobs that are no longer making progress.

Postzyo provides related queue context. Celery's optimization guide discusses arrival and processing capacity; Django documents transactions.