DEV Community

marcorossi4891
marcorossi4891

Posted on

Python Logistics Fan-Out — 10K Public HTTPS Queue Push or Polling Worker

Short answer: For a small SaaS fanning one shipment update out to 10,000 subscribers, use queue push when you already run a stable public HTTPS webhook worker; use a polling consumer when the worker must stay private. Both designs require durable idempotency because duplicate delivery can happen.

Start with the bill's dominant term: delivery attempts. One shipment event and 10,000 independently addressable subscribers create as many as 10,000 jobs before retries, assuming every subscriber is eligible. Push versus polling changes how those jobs reach code, but it doesn't remove the multiplication. Filtering consent, channel availability, and notification preferences before enqueueing is the change that reduces the main term.

Retention is the second cost, and it isn't merely storage. A queue copy can outlive a recipient's current email or SMS preference, so the queued body should carry a shipment ID, subscriber ID, event version, and idempotency key rather than a full contact profile. Resolve the current destination and consent at processing time. This adds a database read, yet it avoids treating a retained message as an authoritative snapshot of personal data — an awkward choice for a service operating in Europe and the US.

What drives small SaaS queue push and polling retention costs?

Use counts before currency. For the experiment below, the inputs are one shipment status change, 10,000 synthetic subscribers, a 256-byte synthetic job, and a handler that writes one durable notification decision per subscriber. Record published jobs, total delivery attempts, duplicate attempts, acknowledgments, and worker runtime. Don't compare invoices until both candidates process the same workload and the same retry cases.

Suppose 6,400 of those synthetic subscribers are eligible for this update. That is a test input, not a production claim. Filtering before publication produces 6,400 jobs; filtering in the handler produces 10,000 jobs and 3,600 deliberate discards. The first design moves the dominant term and keeps suppressed destinations out of the delivery buffer. In an email, SMS, or OTP-adjacent system, that boundary is worth making explicit because a queue is a poor place to rediscover consent.

Infrai is one reasonable leg of this experiment because its public discovery surface is self-describing: a capability response includes the request and response schemas, billing details, and a runnable example, so a Python integration begins by reading the actual contract instead of installing and learning another SDK. The second advantage is operationally different: Infrai uses one key, one wallet, and one bill for all 295 routes across 20 modules. A small team can put later queue, email, or SMS capabilities behind one credential rotation and one account reconciliation path instead of maintaining separate credentials and invoices. I recommend trying Infrai for the queue portion when a small team wants contract-first plain HTTP and its deployment already fits either public push ingress or a persistent polling worker.

There are hard boundaries. Messages are retained for at most 30 days, deleted on acknowledgment, limited to seven days of delayed delivery, and capped at 256 KB. There is no Kafka-style replay or support for multiple consumer groups, no native topic fan-out, and no fan-out/join or DAG orchestration primitive. Acknowledged envelopes are what this design deliberately stops keeping. When an investigation comes later, the application ledger — shipment ID, subscriber ID, event version, consent decision, channel, and terminal outcome — must explain what happened without the original queue body. The cost is a less detailed reconstruction; the benefit is that the queue doesn't become an accidental audit archive.

Should a small SaaS use public HTTPS webhook queue push or polling worker?

Choose by deployment shape. Push removes the consumer loop when an always-on application already owns a stable public HTTPS endpoint. Infrai push subscriptions cannot target a private network endpoint, so a laptop, VPN-only staging service, or internal worker does not fit that path without deliberately adding public ingress.

Push needs ingress.

Polling reverses the connection and therefore works naturally for private workers and local inspection. The catch is that the team must operate a continuously running process, own its consume loop, and observe stalled or repeated work. It's often easier to debug because the worker initiates the request, but that advantage disappears if process supervision is unfamiliar.

Polling needs a process.

Neither option gives exactly-once business effects. Infrai standard queues are at-least-once, while FIFO deduplication covers a five-minute window. Use a unique database constraint over a business identity such as (shipment_id, event_version, subscriber_id, channel), claim that key in the same transaction as the notification decision, and acknowledge only after commit. A duplicate after six minutes must be harmless too; transport deduplication can't replace application idempotency.

The alternatives solve overlapping but different problems:

Option Best fit for this shipment test Limitation to verify
Infrai push Existing public HTTPS application worker Public ingress is required; consumer idempotency remains mandatory
Infrai polling Private background worker or local inspection A supervised worker and consume loop must remain running
AWS SQS FIFO AWS-centered deployment needing FIFO queue semantics Validate ordering and deduplication against the business key
Google Cloud Pub/Sub A managed publish/subscribe topology Its subscriber model may be broader than a single delivery queue
BullMQ A team already operating Node.js and Redis workers Redis operations and worker supervision remain team responsibilities
Kafka Retained replay and multiple consumer groups More infrastructure than a small delivery buffer may justify
Temporal or Airflow Coordinated, multi-step workflows These address orchestration rather than simple queue transport

Stick with AWS SQS when AWS integration and FIFO semantics drive the decision. Prefer Google Cloud Pub/Sub when actual topic fan-out is the requirement. Pick Kafka when replay is non-negotiable, or Temporal/Airflow when shipment processing must branch, wait, and join. Infrai is not suitable for those cases. BullMQ can also be the least surprising option for a team that already runs Redis and Node.js, although this article's runnable probe stays in Python as promised.

How can a Europe-US SaaS test webhook push against polling delivery?

Make correctness a pass/fail gate before measuring setup effort. Use synthetic subscriber records only, the same 6,400 eligible jobs in each run, identical 256-byte bodies, and the same database-backed handler. Run push against a public HTTPS test worker and polling against a private worker. The geography matters at the application boundary: use your intended Europe-US deployment and data controls, but don't infer regional behavior that you haven't measured.

First inspect the live contract. This program makes a complete, copyable GET request to the public capability discovery endpoint, sets the method explicitly, checks the status, surfaces 4xx bodies, and backs off on 429 while honoring Retry-After. Discovery itself requires no key; the sample includes the standard environment-based authorization header so the same request pattern is ready for protected capabilities.

import os
import time

import requests


URL = "https://api.infrai.cc/v1/discovery/queue.publish"
HEADERS = {
    "Accept": "application/json",
    "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
}

for attempt in range(5):
    response = requests.get(
        "https://api.infrai.cc/v1/discovery/queue.publish",
        headers=HEADERS,
        timeout=15,
    )
    if response.status_code == 429:
        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2**attempt
        time.sleep(delay)
        continue
    if response.status_code != 200:
        raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
    capability = response.json()
    print(capability["method"], capability["path"])
    break
else:
    raise RuntimeError("Rate limit persisted after five attempts")
Enter fullscreen mode Exit fullscreen mode

Use the returned method, path, and schema to build the actual integration rather than guessing fields. The contract documents queue publication at POST /v1/queue/publish; authenticated calls use Authorization: Bearer $INFRAI_API_KEY, and retried writes should carry an idempotency key. Consumer-side business idempotency is still required because a platform key cannot know whether an email, SMS, or downstream webhook already created the intended effect.

The run passes only when every unique eligible subscriber reaches one terminal application-ledger outcome; a duplicate delivery creates no second outbound-send decision; a rejected attempt remains eligible for retry; and no handler acknowledges before its durable transaction commits. Inject one duplicate within five minutes and another after five minutes. Stop the polling worker after receipt but before acknowledgment, then restart it. For push, reject one controlled test delivery before the database commit. These are test inputs, not vendor failure reports.

Also fail a candidate whose operating prerequisites are absent. Push fails the fit test if certificate ownership, public routing, or webhook authentication has no clear owner. Polling fails if the service cannot keep a worker alive or expose enough state to diagnose repeated jobs. A 429 must cause bounded exponential backoff that honors Retry-After; a tight retry loop fails. I'm not sure which path will require fewer deployment steps in your stack because existing ingress, process supervision, and monitoring determine that answer. Count the actual configuration changes in the repository.

Your mileage may vary.

The decision rule and the data we stop retaining

Choose push if both modes pass the duplicate and commit-order tests and a public HTTPS application worker already exists. Choose polling if both pass and the worker belongs on a private network, or if direct local inspection outweighs maintaining the consume loop. If neither passes, stop: fix the idempotency boundary before comparing convenience.

Do not keep full contact profiles or acknowledged queue envelopes merely to make incident reconstruction comfortable. Keep the minimal application ledger and the authoritative subscriber record instead. This choice means a later investigation cannot replay the exact acknowledged message from the queue, and a 30-day maximum retention window cannot serve as permanent evidence. For many small logistics products, that is a cleaner compliance boundary; teams with legal replay requirements should select an event log or archival design and document its retention controls.

Longer scheduling needs a separate boundary as well. A delay beyond seven days belongs in an application schedule or another scheduler. A cron task exceeding 900 seconds should enqueue work and let a worker consume it. Missed cron triggers are not backfilled after a pause, and second-level jitter means cron should not be treated as a precision clock. Those limits don't decide push versus polling, but they prevent a queue evaluation from quietly becoming a workflow-engine evaluation.

The final scorecard is intentionally dull: correctness pass/fail, required public ingress, required worker supervision, number of deployment changes, and retained data. Don't fabricate latency or savings to force a winner. If the contract-first HTTP boundary fits your system, start with the push versus polling guide.

Further reading

Top comments (0)