# Background Jobs in the Cloud: Cronjobs vs Workers vs Event Queues
Every non-trivial app has background processing. Sending an email after signup, resizing uploaded images, generating a monthly invoice, syncing data from an external API — none of these should happen inside an HTTP request handler. But how you process them matters.
There are three main patterns, and they're not interchangeable.
1. Cronjobs (Scheduled Tasks)
What they are: Tasks that run on a schedule, regardless of user activity.
Examples:
- Send weekly digest emails every Monday at 8am
- Sync data from a third-party API every hour
- Clean up expired sessions every day at midnight
- Generate monthly billing reports on the first of each month
When to use them: When the task is time-based, not event-triggered. The schedule is the trigger.
On PandaStack: Native cronjob support with container-based execution:
panda cronjob create \
--name "weekly-digest" \
--image your-org/mailer:latest \
--schedule "0 8 * * 1" \
--env MAIL_API_KEY=xxxEach run is logged. You can set alerts for failed executions. Manual trigger with panda cronjob run --name weekly-digest.
2. Background Workers
What they are: Long-running processes that pick tasks off a queue and execute them.
Examples:
- Processing uploaded files after a user submits them
- Sending transactional emails triggered by user actions
- Resizing images, generating thumbnails
- Running expensive computations that would time out in an HTTP request
When to use them: When a user action triggers processing that takes more than a second or two.
The pattern:
// API handler — fast
app.post('/uploads', async (req, res) => {
const job = await queue.add({ fileId: req.file.id })
res.json({ jobId: job.id }) // return immediately
})
// Worker process — runs separately
queue.process(async (job) => {
const file = await getFile(job.data.fileId)
await resize(file)
await notify(file.userId)
})On PandaStack: Deploy the worker as a separate Container App project pointing at the same Docker image but with a different start command. It shares the same database and Redis queue.
3. Event Queues
What they are: Message brokers (RabbitMQ, SQS, Kafka) that decouple producers from consumers.
Examples:
- Order placed → inventory service, notification service, analytics service all process independently
- User signup → onboarding email, welcome gift, CRM sync happen in parallel
- Payment succeeded → update subscription, provision feature access, send receipt
When to use them: When multiple systems need to react to the same event, or when you need guaranteed delivery and retry semantics.
The pattern:
// Producer (in your API)
await rabbitMQ.publish('user.signed_up', { userId, email })
// Consumer 1 (email service)
rabbitMQ.subscribe('user.signed_up', async (msg) => {
await sendWelcomeEmail(msg.email)
})
// Consumer 2 (analytics service)
rabbitMQ.subscribe('user.signed_up', async (msg) => {
await trackSignup(msg.userId)
})On PandaStack: Connect to any external RabbitMQ or use a managed queue service via environment variables. Each consumer is a separate container project.
Decision Framework
| Trigger | Pattern |
|---|---|
| Time-based schedule | Cronjob |
| User action, needs processing < 30s | Background worker with queue |
| User action, multiple systems need to know | Event queue |
| One-off task, triggered manually | Cronjob with manual run |
The Common Mistake
Putting everything in cronjobs. Cronjobs are tempting because they're simple, but a cronjob checking "are there any unprocessed uploads?" every minute is worse than a queue that triggers processing immediately when an upload arrives.
Use the trigger that matches the event. Time → cron. Action → queue.
Full docs: [docs.pandastack.io](https://docs.pandastack.io).