> ## Content Index
> Fetch the complete content index at: https://premlogic.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Building a Cron Job Scheduler Using Redis and Node.js
- URL: https://premlogic.dev/building-a-cron-job-scheduler-using-redis-and-node-js/
- Published: 2024-08-31T12:54:51.000Z
- Updated: 2026-04-27T09:17:51.000Z
- Description: Discover how to build an advanced event-driven scheduler using Redis’ key expiration feature for real-time task management.
- Author: Prémices N. Kamasuwa
- Tags: 2024, Blog, Tech, SystemDesign, Typescript, Javascript, Redis, NodeJS, Scheduling, Cron, TaskQueue, #Import 2026-10-18 21:41

Authors: [Daniel Charles Mwangila](https://github.com/truestbyheart?ref=premlogic.dev) and [Prémices Kamasuwa ](https://premlogic.dev/about)

## Introduction

In this article, we'll explore how to build a robust and flexible scheduling system by combining the strengths of Redis and Node.js. This approach not only replicates the functionality of traditional cron jobs but also enhances it by leveraging Redis to manage task scheduling through expiration events. Redis, known for its high-speed in-memory data storage, becomes an optimal solution for real-time applications that demand precise and efficient task execution.

Throughout this guide, we'll walk through the development of an event-driven scheduling system that utilizes Redis' key expiration feature to trigger tasks at predefined intervals. By the end, you will have a deeper understanding of how Redis can be harnessed for more complex real-time scenarios, enabling you to build systems that go beyond basic task scheduling.

## **Prerequisites**

To follow along with this tutorial, ensure you have the following:

- **Node.js** is installed on your local machine. If not, follow [this guide to install Node.js](https://nodejs.org/en/?ref=premlogic.dev).
- **Redis** is installed locally or accessible via a remote server. You can find installation instructions [here](https://redis.io/download?ref=premlogic.dev).
- Familiarity with **TypeScript** and **Node.js**. If you need to brush up, consider reviewing [the TypeScript documentation](https://www.typescriptlang.org/docs/?ref=premlogic.dev) and [Node.js documentation](https://nodejs.org/en/docs/?ref=premlogic.dev).
- A code editor, such as [Visual Studio Code](https://code.visualstudio.com/download?ref=premlogic.dev).
- Basic knowledge of Redis operations, particularly how to set and get keys and configure keyspace notifications.

## **Step 1 — Setting Up Your Node.js Application**

Let's start by setting up the Node.js environment for our project.

### **1.1\. Create a New Project Directory**

First, create a directory for your project and navigate into it:

```bash
mkdir redis-scheduler
cd redis-scheduler

```

### **1.2\. Initialize a New Node.js Project**

Initialize a new Node.js project using the following command:

```bash
npm init -y

```

This command creates a `package.json` file with default settings.

### **1.3\. Install Necessary Node.js Packages**

Install the required packages for this tutorial:

```bash
npm install redis dotenv date-fns

```

These packages include:

- **redis**: For interacting with Redis.
- **dotenv**: To manage environment variables.
- **date-fns**: For date manipulation.

Next, install the development dependencies:

```bash
npm install @types/node ts-node typescript

```

These packages will help you work with TypeScript and Node.js more effectively.

### **1.4\. Initialize a TypeScript Project**

Initialize a TypeScript configuration file:

```bash
tsc --init

```

This command creates a `tsconfig.json` file in your project root, enabling TypeScript support.

### **1.5\. Project Structure**

To organize your code effectively, set up the following directory structure:

```bash
redis-scheduler/
│
├── src/
│   ├── interfaces/
│   │   └── Task.ts
│   ├── utils/
│   │   └── redisClient.ts
│   ├── handlers/
│   │   ├── classHandler.ts
│   │   └── functionHandler.ts
│   ├── scheduler.ts
│   └── listener.ts
│
├── .env
├── package.json
├── tsconfig.json
└── README.md
```

This structure helps keep your project organized, making it easier to maintain and extend in the future.

## **Step 2 — Implementing the Task Interface**

Create an interface to define the structure of a task. This interface ensures that every task follows a consistent format, which is crucial for the scheduler to function correctly.

Create a new file `src/interfaces/Task.ts`:

```typescript
export interface ExecutionPath {
    file_path: string;   // The path to the file containing the task's function
    class_name: string;  // The class name where the function resides
    function_name: string; // The specific function to execute
}

export interface Task {
    task_id: string;     // Unique identifier for the task
    title: string;       // Human-readable name for the task
    interval: string;    // The interval at which the task should run (e.g., '5m', '1h')
    last_run?: Date;     // The last time the task was executed
    next_run: Date;      // The next scheduled run time
    execution_path: ExecutionPath; // Details on where and what to execute
}

```

### **Explanation of Task Interface Fields:**

- **task\_id**: A unique identifier for the task, ensuring that each task can be tracked separately.
- **title**: A descriptive name for the task. Useful for logging and debugging.
- **interval**: Specifies how frequently the task should run. This could be in seconds (`s`), minutes (`m`), hours (`h`), days (`d`), or weeks (`w`).
- **last\_run**: An optional field that records the last execution time, useful for monitoring.
- **next\_run**: The next time the task is scheduled to run.
- **execution\_path**: Specifies where to find the function to execute, including file path, class, and function names.

## **Step 3 — Implementing the Redis Client**

To interact with Redis, we need to set up a client. This client will be used to set and get tasks and listen for expiration events.

Create a new file `src/utils/redisClient.ts`:

```typescript
import { createClient } from "redis";
import dotenv from "dotenv";
dotenv.config();

// Retrieve Redis connection URI from the environment variables
console.log(process.env.REDIS_URI);
export const redisClient = async () => {
    const client = createClient({ url: process.env.REDIS_URI })
        .on('error', (err) => console.error(`Redis Client Error: ${err}`))
        .on('connect', () => console.info('Connected to Redis'))
        .on('ready', () => console.info('Redis is ready'));

    await client.connect();
    return client;
};

// Function to retrieve data from Redis
export const getFromRedis = async (key: string): Promise<{ [key: string]: any } | null> => {
    const client = await redisClient();
    try {
        const value = await client.get(key);
        if (value) return JSON.parse(value);
        return null;
    } catch (error) {
        console.error(`Error getting key: ${key} from Redis: ${error}`);
        return null;
    } finally {
        await client.disconnect();
    }
}

// Function to store data in Redis with an optional expiration time
export const setToRedis = async (key: string, value: string, expireIn?: number): Promise<void> => {
    const client = await redisClient();
    try {
        await client.set(key, value);
        if (expireIn) {
            await client.expire(key, expireIn);
        }
    } catch (error) {
        console.error(`Error setting key: ${key} to Redis: ${error}`);
    } finally {
        await client.disconnect();
    }
}

```

### **Explanation:**

- **redisClient**: Initializes a connection to Redis and manages connection events.
- **getFromRedis**: Fetches a value from Redis by key and parses it from JSON.
- **setToRedis**: Sets a value in Redis with an optional expiration time, which is crucial for scheduling tasks to run at specific intervals.

## **Step 4 — Implementing the Scheduler Class**

The scheduler class will handle scheduling logic, including calculating the next run time and executing tasks.

Create a new file `src/scheduler.ts`:

```typescript
import { addSeconds, addMinutes, addHours, addDays, addWeeks } from 'date-fns';
import path from 'path';
import { setToRedis, getFromRedis } from './utils/redisClient';
import { Task } from './interfaces/Task';

export class Scheduler {

    // Calculate the next run time based on the interval string
    private calculateNextRun(interval: string): { next_run: Date; interval: number } {
        const now = new Date();
        const match = interval.match(/^(\\d+)(s|m|h|d|w)$/);

        if (!match) {
            throw new Error('Invalid interval format');
        }

        const value = parseInt(match[1], 10);
        const unit = match[2];
        let next_run;
        const seconds_per_unit = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };

        switch (unit) {
            case 's': next_run = addSeconds(now, value); break;
            case 'm': next_run = addMinutes(now, value); break;
            case 'h': next_run = addHours(now, value); break;
            case 'd': next_run = addDays(now, value); break;
            case 'w': next_run = addWeeks(now, value); break;
            default: throw new Error('Unsupported time unit');
        }

        return {
            next_run: next_run,
            interval: value * seconds_per_unit[unit],
        };
    }

    // Update the next run time in Redis
    private async updateNextRun(task: Task) {
        const { next_run, interval } = this.calculateNextRun(task.interval);
        task.next_run = next_run;
        await setToRedis(`SCHEDS:${task.task_id}`, JSON.stringify(task), interval);
    }

    // Execute a function dynamically based on its path
    private async executeFunctionFromPath(task: Task) {
        const { file_path, class_name, function_name } = task.execution_path;
        try {
            const modulePath = path.resolve('./src/handlers', `${file_path}`);
            console.info(`Importing module from path: ${modulePath}`);
            const module = await import(modulePath);

            // Get the class and create an instance
            const instance = new module[class_name]();
            if (typeof instance[function_name] !== 'function') {
                throw new Error(`Function ${function_name} not found in class ${class_name}`);
            }

            // Execute the function
            console.info('Executing scheduled task function...');
            await instance[function_name]();
            console.info('Function executed successfully');
        } catch (error) {
            console.error('Error executing function from path:', error);
        }
    }

    // Main function to run the scheduler
    async runScheduler(scheds_key: string) {
        const task_id = scheds_key.split(':')[1];
        const task = await getFromRedis(`SCHEDS:${task_id}`) as Task;

        if (!task) {
            console.error(`Task with id: ${task_id} not found`);
        } else {
            await this.executeFunctionFromPath(task);
            await this.updateNextRun(task);
        }
    }
}

export const scheduler = new Scheduler();

```

### **Explanation:**

- **calculateNextRun**: Calculates the next run time based on the given interval (e.g., "5m" for 5 minutes). It also calculates the interval in seconds for Redis’ `expire` function.
- **updateNextRun**: Updates the task's next run time in Redis to ensure it triggers again at the correct interval.
- **executeFunctionFromPath**: Dynamically loads and executes a function from a specified file and class path, providing flexibility in what tasks can be scheduled.
- **runScheduler**: The main function that orchestrates the retrieval and execution of scheduled tasks, then updates their next run time.

## **Step 5 — Implementing a Sample Task Handler**

For demonstration purposes, let's create a simple handler that outputs "Hello from the handler" to the console after a 5-second delay.

Create a new file `src/handlers/classHandler.ts`:

### **Explanation:**

- **HelloHandler**: A class with an `execute` method that simulates a delayed task by waiting 5 seconds before logging a message to the console.

## **Step 6 — Implementing the Redis Listener**

The Redis listener monitors key expiration events and triggers the scheduler when a key expires.

Create a new file `src/listener.ts`:

```typescript
import { redisClient } from './utils/redisClient';
import { scheduler } from './scheduler';

(async () => {
    const client = await redisClient();

    // Enable keyspace notifications for expiration events
    client.configSet('notify-keyspace-events', 'Ex');

    const sub = client.duplicate();
    await sub.connect();
    const expired_subKey = '__keyevent@0__:expired';

    // Listen for expired events on all keys
    sub.pSubscribe(expired_subKey, async (key) => {
        console.info(`[i] Key expired: ${key}`);
        await scheduler.runScheduler(key);
    });

    console.info('Redis listener set up and waiting for events...');
})();

```

### **Explanation:**

- **Redis Keyspace Notifications**: Configures Redis to notify when keys expire. This is essential for our scheduling logic as Redis uses the `EXPIRE` command to manage key expiration.
- **Listener Setup**: Subscribes to expiration events and triggers the `runScheduler` function, which handles executing the scheduled task.

## **Step 7 — Running and Testing the Scheduler**

### **7.1\. Configure Environment Variables**

Ensure you have your Redis connection properly configured. Create a `.env` file in the project root if you haven't done so already, and add your Redis URI:

```bash
REDIS_URI=redis://localhost:6379

```

Make sure to replace `localhost:6379` with the appropriate host and port if your Redis instance is hosted elsewhere.

### **7.2\. Schedule a Task Using TypeScript Code**

Instead of using the Redis CLI, we will use a TypeScript script to add a task to Redis. This approach provides more flexibility and demonstrates how to automate task scheduling programmatically.

### **Step-by-Step Guide to Scheduling a Task via TypeScript:**

1. **Create a TypeScript Script to Add a Task**:

Create a new file named `src/scheduleTask.ts` to automate the process of scheduling tasks in Redis:

```typescript
import { v4 as uuidv4 } from 'uuid';
import { setToRedis } from './utils/redisClient';
import { Task } from './interfaces/Task';

// Function to schedule a new task
const scheduleTask = async () => {
    // Generate a unique UUID for the task
    const taskId = uuidv4();

    // Create a new task object
    const newTask: Task = {
        task_id: taskId,
        title: 'Hello World Task',
        interval: '5s',  // Adjust as needed for your testing
        next_run: new Date(),
        execution_path: {
            file_path: 'classHandler',  // This corresponds to src/handlers/classHandler.ts
            class_name: 'HelloHandler',
            function_name: 'execute',
        }
    };

    // Convert the task object to a JSON string
    const taskJSON = JSON.stringify(newTask);

    try {
        // Store the task in Redis without expiration
        await setToRedis(taskId, taskJSON);

        // Set a shadow key with expiration
        const intervalInSeconds = 5; // Example interval in seconds; modify as needed
        await setToRedis(`SCHEDS:${taskId}`, taskId, intervalInSeconds);

        console.info(`Task scheduled successfully with ID: ${taskId}`);
    } catch (error) {
        console.error('Error scheduling task:', error);
    }
};

// Execute the function to schedule the task
scheduleTask();

```

### **Explanation of the Script:**

- **UUID Generation**: We use the `uuid` package to generate a unique identifier (`task_id`) for each task, ensuring no two tasks have the same ID.
- **Task Object Creation**: A `Task` object is created with all the necessary details, including the interval and execution path.
- **Storing the Task in Redis**:
- The task is stored under a key `TASK:<uuid>` without an expiration time.
- A shadow key, `SCHEDS:<uuid>`, is created with an expiration time corresponding to the task's interval. This shadow key will expire and trigger the listener.
- **Expiration Handling**: The `intervalInSeconds` specifies when the shadow key expires, causing Redis to emit an event that our listener can capture.
1. **Install the `uuid` Package**:

If you haven't already installed the `uuid` package, you can add it to your project by running:

```bash
npm install uuid

```

Also, install the TypeScript types for `uuid`:

```bash
npm install @types/uuid --save-dev

```

1. **Run the TypeScript Script**:

Execute the script to add the task to Redis:

```bash
ts-node src/scheduleTask.ts

```

### **7.3\. Start the Redis Listener**

With the task now scheduled, start the Redis listener in a separate terminal window to monitor for expiration events:

```bash
ts-node src/listener.ts

```

### **7.4\. Observe the Output**

After the interval specified in the shadow key (`SCHEDS:<uuid>`) expires, you should see the task execution output:

```
[i] Key expired: SCHEDS:<uuid>
Importing module from path: <path-to-classHandler>
Hello from the handler

```

This output confirms that the Redis listener detected the key expiration and successfully executed the task associated with that key.

### **Conclusion**

Using a TypeScript script to add tasks to Redis programmatically gives you greater flexibility and control over task scheduling. This method demonstrates how to integrate Redis-based scheduling into your applications, leveraging UUIDs for unique task identification and shadow keys for triggering task execution. You can now expand on this foundation to build more complex scheduling systems or integrate it into a broader application framework.