Skip to main content
RapidDev - Software Development Agency
replit-tutorial

How to run background jobs in Replit

Replit supports background job processing through Reserved VM deployments that keep your server running 24/7, multi-process configurations in the .replit file, and job queue libraries like Bull. You can run background tasks using setInterval for simple cases, Bull with Redis for production-grade queuing, or separate worker processes. This tutorial covers all three approaches so you can choose the right one for your use case.

What you'll learn

  • Use setInterval for simple periodic background tasks
  • Build an in-process job queue for moderate workloads
  • Set up Bull with Redis for production-grade job processing
  • Configure .replit to run worker processes alongside your web server
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read20-30 minutesReserved VM deployment required for always-on background jobs. Core ($25/month) or Pro ($100/month) plan required. Autoscale deployments go idle and are not suitable for persistent background work.March 2026RapidDev Engineering Team
TL;DR

Replit supports background job processing through Reserved VM deployments that keep your server running 24/7, multi-process configurations in the .replit file, and job queue libraries like Bull. You can run background tasks using setInterval for simple cases, Bull with Redis for production-grade queuing, or separate worker processes. This tutorial covers all three approaches so you can choose the right one for your use case.

Run Background Jobs in a Node.js Application on Replit

Many applications need to process tasks outside the normal request-response cycle: sending emails, generating reports, cleaning up data, or syncing with external APIs. Replit supports background job processing through several approaches, from simple timers to production-grade job queues. This tutorial covers three patterns — setInterval for lightweight periodic tasks, in-process queues for moderate workloads, and Bull with Redis for complex job processing — so you can pick the right tool for your needs.

Prerequisites

  • A Replit account on Core or Pro plan
  • A Node.js application with an Express server
  • Familiarity with async/await and Promises in JavaScript
  • Understanding of when background processing is needed (tasks that should not block API responses)

Step-by-step guide

1

Understand Replit's deployment types for background work

Not all Replit deployment types support background jobs. Autoscale deployments go idle after 15 minutes of inactivity, which kills any running background processes. Static deployments have no server at all. For background jobs, you need a Reserved VM deployment, which provides an always-on server that runs 24/7. Scheduled Deployments work for periodic tasks but shut down after the script completes. Choose Reserved VM for continuous background processing and Scheduled Deployments for periodic batch jobs.

Expected result: You understand that Reserved VM is the right deployment type for always-on background job processing.

2

Use setInterval for simple periodic tasks

For lightweight tasks that run on a fixed schedule (clearing caches, checking API status, sending heartbeats), setInterval is the simplest approach. Add a setInterval call in your server startup code that runs a function at a specified interval. Wrap the function in a try-catch so a failed job does not crash your server. This approach has no external dependencies and works immediately, but it has limitations: no retry logic, no job persistence, and no concurrency control.

typescript
1// server/jobs/cleanup.js
2export function startCleanupJob(pool) {
3 const INTERVAL = 60 * 60 * 1000; // 1 hour
4
5 async function runCleanup() {
6 try {
7 console.log('[Job] Cleanup started:', new Date().toISOString());
8
9 // Delete old sessions
10 const result = await pool.query(
11 `DELETE FROM sessions WHERE expires_at < NOW()`
12 );
13 console.log(`[Job] Deleted ${result.rowCount} expired sessions`);
14
15 // Delete old logs
16 const logs = await pool.query(
17 `DELETE FROM app_logs WHERE created_at < NOW() - INTERVAL '30 days'`
18 );
19 console.log(`[Job] Deleted ${logs.rowCount} old log entries`);
20 } catch (err) {
21 console.error('[Job] Cleanup failed:', err.message);
22 // Do not throw — let the interval continue
23 }
24 }
25
26 // Run immediately on startup, then every hour
27 runCleanup();
28 setInterval(runCleanup, INTERVAL);
29 console.log('[Job] Cleanup job scheduled every', INTERVAL / 1000, 'seconds');
30}
31
32// In server/index.js:
33import { startCleanupJob } from './jobs/cleanup.js';
34startCleanupJob(pool);

Expected result: The cleanup job runs once on server startup and then every hour, with each execution logged to Console.

3

Build a simple in-process job queue

For tasks that need to be queued and processed sequentially (sending emails, generating PDFs, processing uploads), build a simple in-process queue. This approach processes one job at a time using an array as a buffer and a recursive processor. It handles failures gracefully and retries failed jobs. The limitation is that the queue lives in memory — if the server restarts, pending jobs are lost. For most small to medium applications, this is sufficient.

typescript
1// server/jobs/queue.js
2class SimpleQueue {
3 constructor(name, processor, options = {}) {
4 this.name = name;
5 this.processor = processor;
6 this.maxRetries = options.maxRetries || 3;
7 this.queue = [];
8 this.processing = false;
9 }
10
11 add(data) {
12 this.queue.push({ data, attempts: 0 });
13 console.log(`[${this.name}] Job added. Queue size: ${this.queue.length}`);
14 this.process();
15 }
16
17 async process() {
18 if (this.processing || this.queue.length === 0) return;
19 this.processing = true;
20
21 const job = this.queue.shift();
22 try {
23 await this.processor(job.data);
24 console.log(`[${this.name}] Job completed`);
25 } catch (err) {
26 job.attempts++;
27 if (job.attempts < this.maxRetries) {
28 console.warn(`[${this.name}] Job failed, retrying (${job.attempts}/${this.maxRetries})`);
29 this.queue.push(job);
30 } else {
31 console.error(`[${this.name}] Job failed permanently:`, err.message);
32 }
33 }
34
35 this.processing = false;
36 this.process(); // Process next job
37 }
38}
39
40export default SimpleQueue;

Expected result: Jobs are queued and processed one at a time with automatic retries on failure.

4

Set up Bull for production-grade job queuing

For applications that need reliable, persistent job processing with features like delayed jobs, priority queues, and concurrent workers, use Bull. Bull requires Redis as its backing store. You can use an external Redis provider (Upstash, Redis Cloud) and store the connection URL in Replit Secrets. Install Bull and configure it to connect to your Redis instance. Bull provides automatic retries, job progress tracking, and a dashboard for monitoring.

typescript
1// Install: npm install bull
2// Store REDIS_URL in Tools -> Secrets
3
4// server/jobs/emailQueue.js
5import Queue from 'bull';
6
7const emailQueue = new Queue('email', process.env.REDIS_URL, {
8 defaultJobOptions: {
9 attempts: 3,
10 backoff: {
11 type: 'exponential',
12 delay: 2000
13 },
14 removeOnComplete: 100 // Keep last 100 completed jobs
15 }
16});
17
18// Process jobs
19emailQueue.process(async (job) => {
20 const { to, subject, body } = job.data;
21 console.log(`[Email] Sending to ${to}: ${subject}`);
22
23 // Replace with your email sending logic
24 // await sendEmail(to, subject, body);
25
26 return { sent: true, to };
27});
28
29emailQueue.on('completed', (job, result) => {
30 console.log(`[Email] Job ${job.id} completed:`, result);
31});
32
33emailQueue.on('failed', (job, err) => {
34 console.error(`[Email] Job ${job.id} failed:`, err.message);
35});
36
37// Add jobs from API routes
38export function queueEmail(to, subject, body) {
39 return emailQueue.add({ to, subject, body });
40}
41
42export default emailQueue;

Expected result: Bull processes email jobs with automatic retries, exponential backoff, and persistent queue storage in Redis.

5

Configure .replit for multi-process execution

If your background workers are in separate files, configure .replit to run multiple processes simultaneously. Use the & operator in the run command to start both the web server and the worker process. Use wait to keep the parent process alive. In production, the deployment run command can follow the same pattern. This approach separates concerns: the web server handles HTTP requests while the worker processes jobs from the queue.

typescript
1# .replit Run web server and worker process together
2
3run = "node server/index.js & node server/worker.js & wait"
4
5[deployment]
6build = ["sh", "-c", "npm ci --production=false && npm run build"]
7run = ["sh", "-c", "node server/index.js & node server/worker.js & wait"]
8deploymentTarget = "cloudrun"

Expected result: Both the web server and worker process start together, and Console shows logs from both processes.

6

Add a job management API endpoint

Create API endpoints that let you view job queue status, add jobs manually, and check job history. Protect these endpoints with an admin key. This gives you visibility into your background processing pipeline and lets you trigger jobs on demand for testing or manual operations.

typescript
1// server/routes/jobs.js
2import { Router } from 'express';
3import { queueEmail } from '../jobs/emailQueue.js';
4
5const router = Router();
6
7// Admin auth middleware
8function adminOnly(req, res, next) {
9 if (req.headers['x-admin-key'] !== process.env.ADMIN_KEY) {
10 return res.status(403).json({ error: 'Unauthorized' });
11 }
12 next();
13}
14
15// Queue a new job
16router.post('/api/jobs/email', async (req, res) => {
17 const { to, subject, body } = req.body;
18 const job = await queueEmail(to, subject, body);
19 res.status(201).json({ jobId: job.id, status: 'queued' });
20});
21
22// Check job status (admin only)
23router.get('/api/jobs/stats', adminOnly, async (req, res) => {
24 // For Bull queues:
25 // const counts = await emailQueue.getJobCounts();
26 // res.json(counts);
27
28 res.json({ message: 'Job stats endpoint ready' });
29});
30
31export default router;

Expected result: POST /api/jobs/email adds a job to the queue and returns a job ID. GET /api/jobs/stats shows queue counts.

Complete working example

server/jobs/queue.js
1// server/jobs/queue.js — Simple in-process job queue
2// No external dependencies required
3
4class SimpleQueue {
5 constructor(name, processor, options = {}) {
6 this.name = name;
7 this.processor = processor;
8 this.maxRetries = options.maxRetries || 3;
9 this.retryDelay = options.retryDelay || 5000;
10 this.queue = [];
11 this.processing = false;
12 this.stats = { completed: 0, failed: 0, retried: 0 };
13 }
14
15 add(data, options = {}) {
16 const job = {
17 id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
18 data,
19 attempts: 0,
20 priority: options.priority || 0,
21 addedAt: new Date().toISOString()
22 };
23 this.queue.push(job);
24 this.queue.sort((a, b) => b.priority - a.priority);
25 console.log(`[${this.name}] Job ${job.id} added. Queue: ${this.queue.length}`);
26 this.process();
27 return job.id;
28 }
29
30 async process() {
31 if (this.processing || this.queue.length === 0) return;
32 this.processing = true;
33
34 const job = this.queue.shift();
35 try {
36 console.log(`[${this.name}] Processing job ${job.id} (attempt ${job.attempts + 1})`);
37 await this.processor(job.data);
38 this.stats.completed++;
39 console.log(`[${this.name}] Job ${job.id} completed`);
40 } catch (err) {
41 job.attempts++;
42 if (job.attempts < this.maxRetries) {
43 this.stats.retried++;
44 console.warn(`[${this.name}] Job ${job.id} failed, retry in ${this.retryDelay}ms`);
45 setTimeout(() => {
46 this.queue.push(job);
47 this.process();
48 }, this.retryDelay);
49 } else {
50 this.stats.failed++;
51 console.error(`[${this.name}] Job ${job.id} permanently failed:`, err.message);
52 }
53 }
54
55 this.processing = false;
56 if (this.queue.length > 0) {
57 this.process();
58 }
59 }
60
61 getStats() {
62 return {
63 name: this.name,
64 pending: this.queue.length,
65 ...this.stats
66 };
67 }
68}
69
70export default SimpleQueue;
71
72// Usage example:
73// const emailQueue = new SimpleQueue('email', async (data) => {
74// await sendEmail(data.to, data.subject, data.body);
75// }, { maxRetries: 3, retryDelay: 5000 });
76//
77// emailQueue.add({ to: 'user@example.com', subject: 'Hello', body: 'World' });

Common mistakes when running background jobs in Replit

Why it's a problem: Using Autoscale deployment for background jobs, which kills processes after 15 minutes of no HTTP traffic

How to avoid: Switch to Reserved VM deployment for any app that needs background processes running continuously.

Why it's a problem: Not wrapping job processing in try-catch, causing the entire server to crash when a single job fails

How to avoid: Always wrap the processor function in try-catch. Log the error and let the queue continue processing the next job.

Why it's a problem: Storing job state in memory (in-process queue) for critical tasks that must survive server restarts

How to avoid: Use Bull with Redis or store pending jobs in PostgreSQL for persistence. In-process queues lose all pending jobs on restart.

Why it's a problem: Running intensive background tasks in the same event loop as the web server, causing API response times to spike

How to avoid: Use a separate worker process started via .replit multi-process configuration, or use Bull's separate worker pattern.

Why it's a problem: Not setting a maximum retry count, causing permanently failing jobs to retry forever and consume resources

How to avoid: Set a maxRetries limit (3 to 5 is typical) and log permanently failed jobs for manual investigation.

Best practices

  • Use Reserved VM deployments for always-on background jobs — Autoscale deployments go idle and kill background processes
  • Wrap all background job logic in try-catch blocks so a failed job never crashes your server
  • Start with setInterval for simple periodic tasks before introducing a job queue library
  • Store the Redis connection URL in Replit Secrets, not in code, when using Bull or BullMQ
  • Log every job start, completion, and failure with timestamps for debugging
  • Set a maximum retry count to prevent failing jobs from running indefinitely
  • Use Scheduled Deployments for periodic batch tasks that do not need a continuously running server
  • Separate worker processes from the web server in .replit for cleaner architecture and independent scaling

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Node.js Express app on Replit that needs to send emails in the background without slowing down API responses. Show me three approaches: setInterval for periodic tasks, a simple in-memory queue, and Bull with Redis. Include .replit configuration for running a worker process alongside the web server.

Replit Prompt

Add background job processing to my Express app. Create a SimpleQueue class that processes jobs sequentially with retry logic. Set up a cleanup job that runs every hour to delete old database records. Configure the .replit file to run both the web server and a worker process. Add a /api/jobs/stats endpoint to view queue status.

Frequently asked questions

The Starter plan does not support deployments that stay running. Background jobs only work during active workspace sessions, which is not suitable for production use. Upgrade to Core ($25/month) for Reserved VM deployments.

All running processes stop during redeployment. In-memory queues lose pending jobs. Bull queues with Redis retain pending jobs because the queue state is stored externally. Jobs resume processing when the new deployment starts.

Reserved VM pricing starts at approximately $10 to $20 per month for a shared VM with basic CPU and RAM allocation. The exact cost depends on the resource tier you select. This is predictable monthly billing, unlike Autoscale's usage-based pricing.

Yes. BullMQ is the newer version of Bull with TypeScript support and improved performance. The setup is similar — install bullmq, connect to Redis, and define workers. Both libraries work well on Replit.

Process data in chunks rather than loading everything into memory. Set Node.js memory limits with --max-old-space-size in your .replit run command. Monitor memory usage in the Resources panel and add cleanup jobs that free unused resources.

Yes. RapidDev can design and implement job queue architectures for Replit applications, including Redis setup, worker process configuration, retry strategies, and monitoring dashboards for production workloads.

Upstash offers a free Redis instance with 10,000 commands per day and 256 MB storage. Redis Cloud also has a free tier with 30 MB. Both work with Bull on Replit. Store the connection URL in Tools -> Secrets.

Yes. setInterval is not precise — the actual interval may be slightly longer than specified due to event loop blocking. For tasks that must run at exact times (every day at 3 AM), use Replit's Scheduled Deployments or a cron library like node-cron.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.