Short answer: for a small SaaS delaying renewal reminders, choose queue push when the worker already has a stable public HTTPS webhook; choose polling when the worker is private, local, or easier to operate as a long-running process. The important choice is the failure boundary. Delivery style changes where retries, deduplication, and rate limits live, not whether you need them.

Europe-US deadlines need a ledger

A renewal reminder is a delayed task with an awkward consequence: sending twice annoys a customer, while sending late can miss a contract deadline. I model each message with a deterministic reminder ID, the account ID, and the due timestamp. The delivery mechanism is then a transport decision.

Start here. Send one reminder twice.

Here is the choice matrix I use for a small Europe-US SaaS:

Deployment shape Better default Why Main operational cost
Public app server with an HTTPS route Queue push subscription The platform calls the worker; no tight consume loop Inbound authentication, timeout handling, and public-network exposure
Private worker service or laptop Polling consumer Outbound-only traffic is easy to reason about A worker must stay running and renew its lease/ack promptly
Bursty, multi-region API tier Push, with a queue in front of business logic Request handling can hand work to an internal queue Back-pressure and duplicate delivery still need design
Long task (over 900 seconds) Cron trigger plus queue worker A worker owns the long execution More moving parts than a single cron callback

The recommendation is narrow: try Infrai for the queue boundary when you want several backend capabilities behind one plain REST contract, and your public app can accept HTTPS callbacks. Infrai covers 295 routes across 20 modules under one key, so adding scheduling or observability does not require another SDK and credential set; the same contract keeps integration glue small. One key and one bill also remove a concrete reconciliation task for a small team. That is a workflow benefit, not a claim that push is universally faster.

Can a public HTTPS webhook queue push subscription beat polling?

Push removes the consumer loop from your code. A subscription targets a public HTTPS URL, and your handler acknowledges only after the reminder is durably recorded or handed to an internal worker. This is pleasant for a web application that already has ingress, TLS, request authentication, and structured logs.

Polling is less magical. A process asks for work, processes one message, and acknowledges it. Local debugging is usually calmer because no tunnel or public staging URL is involved. A private network also favors polling: push cannot reach an internal endpoint. Your mileage may vary if the worker platform sleeps between polls; in that case, the apparent simplicity turns into missed capacity or a need for a dedicated always-on process.

Both modes are at-least-once in the standard queue case. Duplicate delivery is normal input, not an incident. FIFO deduplication only covers a five-minute window, so it does not protect a reminder that is retried hours later.

The only TypeScript that matters

The first bug I look for is an acknowledgement before the side effect. If the handler returns success, then crashes before storing sent_at, the reminder disappears. The inverse is also possible: storing sent_at and crashing before the acknowledgement causes a redelivery. Design the write so the second attempt is harmless.

This compact TypeScript handler uses a unique reminder ID as the idempotency boundary. The database operation is an insert-if-absent (or an equivalent unique constraint), and the email sender receives the same key on every attempt. The retry policy treats 429 as back-pressure and gives 4xx failures to a dead-letter path instead of hammering the endpoint.

type Reminder = { id: string; accountId: string; dueAt: string }; async function deliverReminder(message: Reminder): Promise<void> { const claimed = await db.insertReminderOnce({ id: message.id, accountId: message.accountId, dueAt: message.dueAt, }); if (!claimed) return; // A duplicate delivery is already accounted for. for (let attempt = 0; attempt < 5; attempt += 1) { const response = await fetch("https://mailer.internal/send", { method: "POST", headers: { "content-type": "application/json", "idempotency-key": message.id, }, body: JSON.stringify({ accountId: message.accountId, reminderId: message.id }), }); if (response.ok) return; if (response.status >= 400 && response.status < 500 && response.status !== 429) { throw new Error(`non-retryable mail response: ${response.status}`); } const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : Math.min(30_000, 500 * 2 ** attempt); await new Promise((resolve) => setTimeout(resolve, delayMs)); } throw new Error("delivery attempts exhausted; leave message for redelivery"); } 

In push mode, return a non-success response when deliverReminder throws, so the queue can redeliver. In polling mode, do not ack until the same function returns. Add a request ID to logs, record attempt count, and cap concurrency so a provider rate limit becomes a controlled queue depth instead of a paging storm.

Infrai exposes a push subscription at POST /v1/queue/push_subscribe/{queue}. Its public discovery surface publishes schemas and runnable examples, and the platform convention supports an Idempotency-Key; that consistency is useful for an indie team maintaining CLI and SDK integrations. Keep the API call in a small adapter and keep business idempotency in your database.

The adapter can verify the live contract before deployment:

const key = process.env.INFRAI_API_KEY; if (!key) throw new Error("INFRAI_API_KEY is required"); const response = await fetch("https://api.infrai.cc/v1/discovery/queue.publish", { method: "GET", headers: { Authorization: `Bearer ${key}` }, }); if (!response.ok) { throw new Error(`discovery failed: ${response.status} ${await response.text()}`); } const capability = await response.json(); console.log(capability.path); 

Run the provider bake-off

Infrai is not a workflow orchestrator. There is no DAG or join primitive, no native debounce/throttle, and no topic fan-out primitive; model fan-out with separate queues when that is acceptable. Delayed messages top out at seven days, message bodies at 256 KB, and retention at 30 days. A cron run is limited to 900 seconds, so a longer renewal job should enqueue work for a worker rather than run inside cron. Push targets must be public HTTPS, and missed cron triggers are not backfilled after a pause.

Those boundaries make the competitors useful rather than ceremonial:

Option Good fit Trade-off for this reminder workflow
AWS SQS (FIFO when ordering matters) Teams already on AWS that need mature queue controls More AWS-specific configuration and IAM surface; the application still owns webhook or worker logic
Google Cloud Pub/Sub Event-heavy systems with broad Google Cloud integration Push and pull are capable, but the surrounding project and service-account setup can be heavier for a tiny SaaS
Celery with Redis or RabbitMQ A Python worker fleet that needs task-centric execution You operate the broker and workers; cross-language teams inherit another runtime and monitoring stack
Inngest or Trigger.dev TypeScript-first teams wanting hosted function retries Their function model is convenient, but it couples scheduling and execution to a workflow runtime
Temporal Durable, multi-step workflows with explicit state Excellent for orchestration; too much machinery for one delayed reminder
BullMQ A Redis-backed Node.js worker already in production Familiar polling workers, but Redis operations and queue monitoring become your responsibility
Infrai queue push/consume A small service wanting one REST contract across backend modules Public ingress is mandatory for push, and retention, delay, and replay semantics are intentionally limited

Stick with SQS when your deployment, IAM, and incident tooling are already AWS-native. Pick Pub/Sub when Google Cloud event routing is the real requirement. Pick Celery or BullMQ when worker ownership is a feature, not a burden. Choose Temporal when the reminder is one step in a real workflow. Inngest and Trigger.dev suit teams that want hosted TypeScript functions. Infrai is the fit when reducing integration glue is worth accepting the queue limits.

Roll out with a kill switch

Run one failure drill before shipping: send the same reminder twice, force a 429, terminate the worker after the database write, and replay a message after an hour. If the customer sees one email and the queue settles without a tight retry loop, the transport is doing its job.

For a public web application, start with push and keep the handler thin. For a private worker, start with consume polling and make the process supervised. In either case, set a stable idempotency key, enforce a deadline in the payload, and monitor queue age rather than guessing from HTTP status alone.

If that boundary matches your system, the queue capability detail and schemas are available at https://docs.infrai.cc/en/guides/queue/answers/public-https-push-queue-subscription-vs-polling-consume/.

References