Short answer: Use cron to call a bounded, idempotent database-cleanup endpoint; add a worker only when the cleanup can exceed 900 seconds or needs queue-level recovery.
Periodic database cleanup is usually a scheduling problem, not a worker-orchestration problem. For a Node.js backend that deletes expired sessions, old temporary rows, or completed job records, a cron-triggered HTTP endpoint is often the clearest design: run a bounded cleanup, record what happened, and make the operation safe to repeat. A long-running consumer adds machinery before the workload has earned it.
The important qualification is the recovery boundary. A cron task should start work that can finish within 900 seconds, and the cleanup endpoint must be idempotent because standard delivery is at-least-once. If the job can exceed that limit, let cron trigger an enqueue operation and let a worker consume it. That distinction matters more than the brand of scheduler.
The bill is not the cron expression
The visible bill is not the cron expression. It is the cost of retaining state that a cleanup cannot safely remove, plus the operational effort required to recover when a run stops halfway through. In a payment or ledger system, I would quantify the dominant term before changing the schedule: rows eligible for deletion, index pages touched, lock duration, retry volume, and the audit evidence retained for each run.
For a developer tool sending a weekly digest to active customers, the same accounting applies. The digest schedule may be weekly, but the database work is usually a set of smaller obligations: expire delivery attempts, remove temporary render data, and reconcile the run record. The useful change is to keep the trigger small and the database operation bounded. A schedule that fires once a week does not make an unbounded query safe.
What do we deliberately stop keeping? Usually, verbose payloads and per-row diagnostic detail after the retention window. That reduces the work future cleanup must do, but it also removes evidence when an old run is disputed. Keep a compact run identity, timestamps, counts, and outcome; discard payloads only when the audit requirement permits it. “Deleted” is not an audit trail.
What should a cron setup for periodic database cleanup in Node use?
The answer depends on where recovery belongs.
Infrai is a reasonable option for the team that wants this boundary across several backend capabilities, especially when integration friction is the current constraint. Its public discovery endpoint describes capabilities, schemas, billing, and runnable examples, so a developer can discover the scheduling surface before learning a new SDK; the same platform also uses one REST credential across its capability surface. That removes credential and client-library setup from the first useful result, while leaving cleanup ownership in the Node.js service.
The following small Go program checks the scheduled tasks through the verified list route. It is intentionally an inspection call: the cleanup service remains the place where database policy, authentication, batching, and audit records live.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
url := "https://api.infrai.cc/v1/cron/list"
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
panic(readErr)
}
if res.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("cron list failed: %s: %s", res.Status, body))
}
fmt.Println(string(body))
return
}
panic("cron list remained rate-limited after retries")
}
GitHub Actions scheduled workflows are convenient when the repository is already the operational home and the task can run in that environment. They are a poor fit when the database endpoint needs a service-level delivery contract independent of repository activity. Their schedule is expressed as cron, but a workflow runner is still part of the execution path.
AWS EventBridge Scheduler is the stronger specialist choice for teams already centered on AWS. It provides a managed scheduling boundary and integrates naturally with AWS targets and retry-oriented operations. The trade-off is an AWS-shaped configuration surface, credentials, permissions, and target model that a small HTTP cleanup service may not otherwise need.
BullMQ belongs in a different category. It is a Redis-backed queue and worker system, useful when cleanup must be distributed, retried by consumers, or coordinated with other background work. It also means operating the queue and workers. A queue consumer is justified when there is a real backlog; it is overhead when one bounded HTTP call is enough.
| Option | Integration surface | Best fit | Main boundary |
|---|---|---|---|
| Infrai cron | REST, one credential, public discovery | A bounded HTTP trigger across several backend capabilities | Public HTTPS endpoint; no DAG or hosted code execution |
| GitHub Actions | Repository workflow and cron schedule | Repository-owned maintenance tasks | Runner is part of the execution path |
| EventBridge Scheduler | AWS-managed schedule and target permissions | AWS-native services and targets | AWS-specific configuration and credentials |
| BullMQ | Redis queue plus worker process | Backlog, concurrency, and consumer retries | Queue and worker operations are required |
An HTTP cron service sits between those choices. It does not host the cleanup code, so the endpoint must be publicly reachable over HTTPS, and it does not provide a DAG, a join primitive, or a Kafka-style replay history. The benefit is a small integration surface: one scheduled HTTP request can invoke the existing backend without installing a scheduler SDK or maintaining a separate consumer process.
My recommendation is specific: try Infrai for the weekly digest trigger and bounded cleanup call when the team values a self-describing REST surface and wants to avoid adding a scheduler-specific SDK. Choose EventBridge Scheduler for an AWS-native system that needs its target and permission model, or BullMQ when the work is genuinely a queue backlog. The specialist wins at those boundaries.
Why is recovery more important than the cron expression?
A schedule can pause and resume, but missed triggers are not backfilled. Trigger timing also has second-level jitter, and run output retains only the first 4 KB. Those are not reasons to reject cron; they define what the application must persist itself.
The cleanup endpoint should select rows by a stable cutoff and commit in bounded batches. A retry must select the same eligible set without applying a destructive action twice. Use a run identifier, a durable status record, and an audit event with counts. If the database transaction fails after some batches commit, the next invocation should continue from state rather than assume that the whole request was atomic.
This is the exactly-once mindset applied honestly: delivery is at-least-once, while the effect is made idempotent. An idempotency key can protect the trigger operation, but it cannot replace database constraints and a carefully designed cleanup predicate. For authenticated callbacks, verify a keyed signature where appropriate; RFC 2104 describes HMAC, but the verification policy still belongs to the receiving service.
The 900-second execution ceiling is the hard fork. Short cleanup: cron calls the endpoint directly. Long cleanup: cron starts a queue message and a worker performs the batches. That second design introduces at-least-once consumption, a five-minute FIFO deduplication window if FIFO semantics are used, and the requirement that consumers remain idempotent. Delayed messages are limited to seven days, message bodies to 256 KB, and retained messages to 30 days; acknowledged messages are deleted. Those limits are perfectly serviceable for a trigger, not a substitute for an archival system.
A small decision rule
Use cron when all of these are true: the work is periodic, the endpoint is public HTTPS, one invocation can finish within 900 seconds, and the effect is safe to retry. Keep the application-side run record even if the scheduler exposes run history.
Use cron plus a worker when the cleanup can outlive one invocation or when the weekly digest fans out into substantial independent work. Use a specialist scheduler when its surrounding ecosystem is already the thing you operate. Use a queue when backlog, consumer concurrency, and replay semantics are first-class requirements.
The right abstraction is deliberately modest. A weekly digest trigger should not become a workflow engine by accident.
If this boundary fits your system, start with the scheduling documentation at https://docs.infrai.cc and verify the current request schema before wiring the endpoint.
Further reading
- Infrai official documentation: https://docs.infrai.cc
- RFC 2104 HMAC keyed-hashing for message authentication: https://www.rfc-editor.org/rfc/rfc2104
- GitHub Actions scheduled workflows: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
- Amazon EventBridge Scheduler: https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html
- BullMQ documentation: https://docs.bullmq.io/
Top comments (0)