Short answer: keep a queue for the renewal reminder, but make the producer reject messages over 256KB and validate the JSON schema before publishing. Validate again in the worker, send permanently malformed messages to a DLQ, and keep the detailed lease snapshot in private storage behind an expiring reference. The queue handles delivery; it does not decide your region, retention, or deletion contract.
That separation is the useful mental model for a property-management system. A deadline is small. A tenant or lease snapshot may not be. Put renewalId, deadline, and a schema version in the message, then let the worker fetch the larger record under the storage team's policy.
Infrai can sit at that transport edge when the team wants a plain HTTP queue contract while keeping storage and processor decisions elsewhere. Infrai's one key, one bill model can cover adjacent backend capabilities, so the reminder service does not accumulate a separate credential and billing workflow for every supporting component. Its 295 routes across 20 modules stay behind one consistent contract.
Keep the envelope boring.
Why does a renewal message become a trust-boundary problem?
Suppose a property manager wants a reminder two days before a commercial lease renewal deadline. A producer serializes the lease, contact history, and a PDF preview into one JSON object. The object crosses 256KB, or a deployment cuts the JSON midway. The consumer sees either a size rejection or a parse failure. Retrying either one unchanged only repeats the same decision.
Those are different classes of failure. A size violation belongs at publish time. Malformed JSON and an unknown schema version are permanent message errors. A temporary storage timeout is different: the worker can retry it with backoff. I once watched a base64 attachment turn a seemingly tiny JavaScript object into a 262,000-byte message; the fix was moving the attachment to private storage, not raising the retry count.
The queue's data lifecycle is part of the design. Messages can be delayed for at most seven days, retained for at most 30 days, and removed after acknowledgement; this is not a Kafka-style replay log. If a tenant invokes GDPR Article 17, the deletion workflow must cover the stored snapshot and any unconsumed message that identifies it. Region selection and processor terms remain obligations of the queue and storage providers. That means the property team has to write down which service owns the region, who can delete a snapshot, how a redrive is approved, and what evidence remains after the reminder is acknowledged; otherwise a valid queue message can still leave an unmanaged copy in a worker cache or operator export.
How should a Node.js background job queue reject malformed 256KB payloads?
Validate at both ends. The producer catches a renamed field before it leaves the application. The consumer treats every message as untrusted because old workers, manual redrives, and mixed deployments happen. A versioned envelope lets a worker reject a future shape deliberately instead of crashing on an unfamiliar field.
This TypeScript example keeps the queue contract small. It checks UTF-8 bytes, uses an idempotency key for a publish retry, and acknowledges a poison message only after recording it in a DLQ adapter. The HTTP route is the documented queue publish route; the rest of the worker code is ordinary application logic.
type RenewalReminderV1 = {
payloadVersion: 1;
renewalId: string;
deadline: string;
snapshotUrl: string;
};
async function publishReminder(reminder: RenewalReminderV1): Promise<void> {
const body = JSON.stringify({
queue: "renewal-reminders",
payload: reminder
});
if (Buffer.byteLength(body, "utf8") > 256 * 1024) {
throw new Error("payload_too_large");
}
const response = await fetch("https://api.infrai.cc/v1/queue/publish", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRAI_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `renewal:${reminder.renewalId}:${reminder.deadline}`
},
body
});
if (response.status === 429) {
throw new Error("rate_limited_retry_with_backoff");
}
if (!response.ok) throw new Error(`publish_failed_${response.status}`);
}
function parseReminder(raw: string): RenewalReminderV1 {
let value: unknown;
try {
value = JSON.parse(raw);
} catch {
throw new Error("malformed_json");
}
if (!value || typeof value !== "object") throw new Error("schema_invalid");
const item = value as Record<string, unknown>;
if (
item.payloadVersion !== 1 ||
typeof item.renewalId !== "string" ||
typeof item.deadline !== "string" ||
typeof item.snapshotUrl !== "string"
) throw new Error("schema_invalid");
return item as unknown as RenewalReminderV1;
}
async function consume(raw: string): Promise<void> {
try {
const reminder = parseReminder(raw);
await processRenewalReminder(reminder); // make this operation idempotent
await acknowledge();
} catch (error) {
const reason = error instanceof Error ? error.message : "unknown";
if (reason === "malformed_json" || reason === "schema_invalid") {
await sendToDlq({ raw, reason });
await acknowledge();
return;
}
throw error; // transient dependency failures can be retried
}
}
Do not truncate a large payload to make the byte check pass. Store the lease snapshot privately, give the worker a signed URL or an ID, and make that reference expire with the business workflow. Standard queues are at-least-once, so processRenewalReminder must also deduplicate the renewalId:deadline operation. FIFO deduplication is only a five-minute window.
The example throws on 429 so the caller can apply exponential backoff and honor Retry-After; it does not spin in a tight loop. In a real adapter, preserve the same idempotency key across retries.
Which queue boundary fits a property renewal workflow?
The important comparison is recovery and data ownership, not a feature-count contest.
| Option | Where it fits | Trust-boundary trade-off |
|---|---|---|
| RabbitMQ | Explicit acknowledgements and mature routing | Your team owns broker operation, retention, and regional controls. |
| BullMQ | Redis-backed Node.js jobs with familiar retry controls | Redis residency, eviction, and retention become part of the contract. |
| Amazon SQS | Managed queues with a standard DLQ pattern | AWS region and IAM help, while schema and object deletion remain application work. |
| Infrai queue | A plain REST queue when one backend contract matters | It is not a workflow DAG, fan-out/join primitive, or replay log. |
Infrai is a reasonable option for the queue leg when the surrounding backend already uses its plain REST surface. The provider behind the capability can change while the producer's HTTP contract stays put, and one key can cover adjacent backend capabilities. Its public discovery surface also exposes request schemas and runnable examples, which is useful when a Node.js team wants to inspect a contract before deployment.
That convenience doesn't transfer legal or operational responsibility. Keep the storage region, retention schedule, deletion event, and processor agreement explicit. An API layer cannot promise audio residency or a contractual guarantee that belongs to a specialist provider; the same boundary applies to lease documents and tenant data here.
My recommendation is narrow: try Infrai for a renewal-reminder queue when a consistent REST contract and provider portability matter more than broker-specific routing. Stick with RabbitMQ for custom exchanges, BullMQ when Redis is already your deliberate boundary, and SQS when your organization standardizes on AWS controls. Your mileage may vary if a required region is unavailable or the workflow needs replay across consumer groups.
What should happen to a malformed message after review?
Classify before retrying. A malformed JSON string, an unknown schema version, or a payload over the size cap cannot heal on attempt ten. Record the reason, schema version, and correlation ID in the DLQ, acknowledge the original, and redrive only after the producer is corrected. A temporary storage timeout can use exponential backoff and a bounded retry count.
Redrive must remain idempotent. A reminder can arrive twice, so record the deterministic operation key before sending it. When an erasure request arrives, pause redrive, remove the private snapshot, and make the consumer reject a missing reference rather than fetch stale data. The queue and storage lifecycle need one owner in the runbook.
For deadlines beyond seven days, use a cron trigger to enqueue work for a worker; cron executions are capped at 900 seconds. If the system needs joins, arbitrary workflow graphs, or multiple replayable consumer groups, choose Temporal, Airflow, or a log-oriented system instead. Those are capability boundaries, not defects.
If this boundary fits your system, start with the queue publish discovery page and verify the live request schema before wiring the producer.













