Async Jobs
Background job processing with Cloudflare Queues, a dead-letter queue, and hourly Cron.
Architecture
Nuxfire processes background work with two independent Cloudflare primitives:
- Queue-based jobs — Cloudflare Queues, behind the
jobsQueueflag inconfig.ts. - Cron — a Cloudflare Cron Trigger, deployed unconditionally today;
infra/cron.tsdoesn't currently check thecronflag declared inconfig.ts'sflagstype.
Queue consumer
apps/functions/src/jobs-consumer.ts processes messages in batches. On success it marks the job completed and writes a job completed log line; on failure it marks the job failed, writes the real error text to a log line, and calls message.retry():
for (const message of batch.messages) {
const job = message.body as typeof schema.jobs.$inferSelect;
try {
await db.update(schema.jobs).set({ status: "completed" }).where(eq(schema.jobs.id, job.id));
await db.insert(schema.logs).values({ jobId: job.id, message: "job completed" });
message.ack();
} catch (err) {
await db.update(schema.jobs).set({ status: "failed" }).where(eq(schema.jobs.id, job.id));
await db.insert(schema.logs).values({
jobId: job.id,
message: `job failed: ${err instanceof Error ? err.message : String(err)}`,
});
message.retry(); // governed by infra/jobs.ts's retry_delay/max_retries
}
}
Dead-letter queue
Once Cloudflare Queues exhausts the retries configured in infra/jobs.ts, the message routes to JobDLQ. apps/functions/src/dlq-consumer.ts reads the job's most recent log line (written by the consumer above) as the failure reason, inserts a DeadLetterRecord holding the original Job object verbatim, and acknowledges — it never retries itself. Replaying a dead-lettered message is a deliberate human action, not automatic.
Cron
apps/functions/src/cron.ts runs on the schedule set in infra/cron.ts (0 * * * * — every hour) and writes a Log row; jobId defaults to "cron" for these scheduled runs, distinguishing them from queue-job logs.
Configuration
export const jobsDeadLetterQueue = flags.jobsQueue
? new sst.cloudflare.Queue("JobDLQ")
: undefined;
export const jobsQueue = flags.jobsQueue
? new sst.cloudflare.Queue("JobQueue", {
maxConcurrency: 5,
dlq: { queue: jobsDeadLetterQueue!.nodes.queue.queueName, retry: 3, retryDelay: "60 seconds" },
})
: undefined;
export const emails = new sst.cloudflare.Cron("Cron", {
schedules: ["0 * * * *"],
job: { handler: "apps/functions/src/cron.ts", link: [database] },
});
Admin operations
Queue health, dead-letter messages, and recent cron log lines are surfaced at /admin/queue — see Platform Admin Console. Replaying a dead-lettered message re-sends the original Job object to JobQueue and stamps replayedAt (never cleared, so a message can only be replayed once from the console without a direct database edit).
Database Schema
Job—id,message,status(pending/processing/completed/failed),teamId, timestamps.Log—id,message,jobId(defaults to"cron"for scheduled runs),createdAt.DeadLetterRecord—id,message(jsonb — the whole originalJobobject, so it can be replayed as-is),error,teamId(nullable — a malformed message can still be recorded),createdAt,replayedAt(null= never replayed).
Current scope
Job creation isn't exposed to tenants today. jobsQueue.send() is called only from the admin replay action (admin.queue.replayDeadLetter) — there's no tenant-facing UI or tRPC procedure yet that enqueues a new job. The queue and cron infrastructure are real and process actual Worker-originated messages; a self-service tenant job queue was intentionally scoped out of the current release.
Ready to build and launch your SaaS?
Get 100% full source code ownership, zero proprietary wrappers, and architecture engineered for millions of requests on Cloudflare.