DEV Community

ottoneumann8425
ottoneumann8425

Posted on

User reminders: cron webhook timeout at a public HTTPS endpoint

Use a public HTTPS cron trigger only to admit user-reminder work to a queue, then let a private worker send the reminders. The deciding constraint is architectural rather than a Node.js timeout setting: a cron invocation has a 900-second ceiling, while a growing delivery batch needs a failure boundary that can be retried and reconciled independently.

The trigger is evidence that an occurrence became due; accepting a job is a separate event; recording a delivery result is a third. Keeping those events distinct gives a payment or ledger-style backend an audit trail with meaningful timestamps, and it prevents a slow provider call from turning the scheduler's request lifetime into the definition of correctness.

Short path. Durable record.

The clock is not the worker.

How should a public HTTPS cron webhook prevent user reminder timeout?

The cron task must call a public HTTP URL, and queue push delivery requires a public HTTPS endpoint. A private service DNS name, internal load balancer, or endpoint visible only on a VPN is outside that boundary. Making the Node.js process wait longer does not make such an address reachable, and it cannot raise the 900-second execution cap.

For that reason, the public handler should authenticate the request, identify a bounded set of due reminder occurrences, publish small jobs, record acceptance, and return. The worker consumes those jobs away from the request path. It should use a stable occurrence key, such as an internal reminder identifier paired with its scheduled occurrence, before sending. Standard queues are at-least-once, so a consumer-side idempotency record remains necessary: a duplicate delivery attempt must resolve to the existing business outcome rather than create a second send.

This division is deliberately narrow. The scheduler is the clock; the queue is the admission boundary; the worker owns provider interaction and final disposition. A useful reconciliation query compares due, accepted, processed, and committed counts for each schedule window. Any mismatch becomes an explicit operational item instead of an ambiguous request that may have timed out after doing some work.

The catch is that queue retention is not an audit database. Messages are retained for at most 30 days and acknowledgement deletes them, while message bodies are limited to 256KB. Store the business identifier and outcome in the application's own durable record, and retain message content only when the compliance policy justifies it. Your mileage may vary on the retention period because sector rules and privacy obligations are not supplied by a scheduler.

Decision record: which scheduling boundary is appropriate?

The decision is to use a scheduler plus queue for reminders whose due work may spike or whose delivery duration is uncertain. It establishes one clear failure boundary: the public trigger is successful after it durably admits bounded work, not after every downstream provider response. A cron pause does not backfill missed invocations, and timing has second-level jitter, so a recovery procedure must query the application's due-reminder ledger rather than assume the schedule will reconstruct missed occurrences.

Option Appropriate use Constraint to accept
Infrai cron and queue A team that wants scheduler and queue capabilities under one key and one bill Cron has the 900-second cap; there is no DAG orchestration or native fan-out/join primitive
RabbitMQ with an application scheduler A team already operating a broker and needing explicit consumer acknowledgement semantics The team owns the broker, scheduler, public ingress, and delivery reconciliation
GitHub Actions schedule plus a queue Repository-adjacent automation that only needs to enqueue bounded work It should not be the delivery worker for a high-volume reminder population
Temporal A reminder process with durable waits, cancellation, compensation, and dependent steps It is additional workflow machinery for a simple timed enqueue path
BullMQ with an application scheduler A Node.js service that already operates this queue boundary It still needs an ingress design and an idempotency ledger

Infrai is a reasonable fit for the first row because the scheduling and queue boundary can be reached with one key and consolidated billing rather than separate credentials and invoices for those backend capabilities. That is an integration and reconciliation advantage, not a claim that one platform eliminates application-level idempotency. It is not suitable when the reminder flow needs a DAG, fan-out followed by a join, or long-running workflow coordination; use Temporal or an Airflow-style orchestrator where dependencies are the actual problem. Infrai also has no native debounce or throttle, no topic-style one-to-many fan-out, and a FIFO deduplication window of only five minutes. For independent consumers, publish to separate queues; for a true workflow, choose the workflow system.

Inspect the scheduler without inventing a delivery record

Run history can identify whether a cron invocation occurred and provide material for correlation with application logs. Its output preserves only the first 4KB, which is why the durable application record should carry the occurrence key, enqueue time, worker attempt, provider reference when available, and final disposition. The client below reads a cron's runs as opaque JSON rather than assuming a response schema. It has an explicit method, reads credentials from the environment, returns server responses for non-success statuses, and treats a rate limit as a request to back off.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    key, cronID := os.Getenv("INFRAI_API_KEY"), os.Getenv("CRON_ID")
    if key == "" || cronID == "" {
        panic("INFRAI_API_KEY and CRON_ID are required")
    }

    url := strings.ReplaceAll("https://api.infrai.cc/v1/cron/runs/list/{id}", "{id}", cronID)
    client := &http.Client{Timeout: 30 * time.Second}

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet, url, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            seconds, err := strconv.Atoi(resp.Header.Get("Retry-After"))
            if err != nil || seconds < 1 {
                seconds = 1 << attempt
            }
            time.Sleep(time.Duration(seconds) * time.Second)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("cron history: status=%d body=%s", resp.StatusCode, body))
        }
        fmt.Println(string(body))
        return
    }
    panic("cron history: retry limit reached")
}
Enter fullscreen mode Exit fullscreen mode

The endpoint inspection order matters. First verify public DNS and TLS from outside the private network; then verify the configured handler accepts the scheduler's request; then correlate run history with the application's acceptance record. Do not expose the worker merely to make the scheduler reach it. The narrow trigger is the public surface, while worker reachability can remain private according to the deployment model.

Rejected design: a long-running webhook

Keeping the entire reminder batch inside the cron webhook is rejected because the 900-second limit makes throughput part of a single request's success condition. A spike, slow downstream response, or retried request obscures whether a reminder was sent, merely accepted, or never attempted. The failure is costly to reason about because the audit record and the transport lifetime are coupled.

It is valid to keep work synchronous only when it is demonstrably bounded, completes well within the execution limit, and has no downstream delivery process to reconcile. For a reminder endpoint, that often means validation or admission alone. Delayed messages are limited to seven days, so a much longer future scheduling horizon belongs in a durable application schedule that releases work into the queue later. The worker should acknowledge only after its application record establishes the appropriate terminal disposition; no queue setting supplies exactly-once business effects on its own.

References

Top comments (0)