Session is an important concept that every developer should understand, without it, we wouldn’t be able to build any app that needs to keep track of users. Don’t immediately think about registered users only, think about guest users too. Storing information about users has many use cases, for example, lets think about a weather app. You, as a user, don’t need to be registered to get the weather information of your city. But wouldn’t it be great if the website remembers your city and shows it on the homepage automatically every time you open it, instead of asking you to enter it again? Of course this information doesn’t need to be stored in session, it can be stored in a local storage of your browser as well. However, the key point here is that we need a way to store information about users and retrieve it later, and that’s where sessions come in.

Managing sessions effectively becomes crucial as the number of users grows. You start encountering issues like memory consumption of your server is being pretty high or losing user sessions during server restarts and that’s where Redis comes in as a game-changer for session management. In this post, I will be explaining how to implement session management with Redis in an Express.js application.

Why Your Session Management Approach Matters

Most applications start with the default in-memory session storage, which works fine for development stages but creates serious problems in production:

  • Server restarts wipe all sessions: Since the session data is stored in the server’s memory, when the server restarts, all of the session data is lost. Users will be forced to log in again.
  • Memory usage increases: As the number of active users grows, the memory usage of the server will increase, which can lead to performance issues, and you might need to vertically scale your server.
  • Scaling horizontally: If you have multiple server instances, each instance will have its own session data, and can’t access the session data of other instances. This can lead to issues when a user is redirected to a different server instance.

Why Redis is the Perfect Session Store

Redis is an extremely fast in-memory data store that can persist data, making it ideal for session storage. Here are the key reasons why it’s better than the default memory store:

  • Persistence: You don’t need to worry about sessions being lost during server restarts, the data is stored in Redis.
  • Shared storage: Multiple app instances can access the same sessions, so you don’t need to worry about how to share the session data between the server instances.
  • Performance: Redis is extremely fast with sub-millisecond response times.
  • Memory management: Redis handles memory efficiently with features like expiration dates, you can automatically delete the session data of stale sessions, and can free up memory for new sessions.

Setting Up Redis Sessions in Express.js

Let’s create a very simple Express.js application to demonstrate how we can implement session management with Redis.

We should start with initializing an empty project, and then install the required packages for the demo.

1
npm install express express-session connect-redis redis

After installing the packages, we can start with the implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const express = require('express');
const session = require('express-session');
const { createClient } = require('redis');
const RedisStore = require('connect-redis').default;

const app = express();
const port = process.env.PORT || 3000;

// Initialize Redis client
const redisClient = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379'
});

redisClient.connect().catch(console.error);

// Configure session middleware
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'your_secret_key', // use a strong secret in production
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // use secure cookies in production
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Sample routes
app.get('/', (req, res) => {
  req.session.viewCount = (req.session.viewCount || 0) + 1;
  res.send(`You have visited this page ${req.session.viewCount} times!`);
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

If we look closely at the code, we can see that:

  1. First, we created a Redis client that connects to our Redis server.
  2. Then, we configured Express to use Redis as our session store.
  3. Then, we implemented a simple counter that demonstrates session persistence.

We have initiliazed the Redis client, with some parameters:

  • resave: We have set it to false because we don’t want to save the session if nothing changed.
  • saveUninitialized: This is set to false because we don’t want to save empty session to conserve resources.
  • cookie: Set sensible defaults for our cookies (secure in production, HTTP-only for security).

Tip: You should never hardcode the session secret in your codebase. Use environment variables for your secrets.

Handling Session Expiration Effectively

Redis, like any other tools, is a finite resource. We just can’t put everything in there and expect it to work forever with the same performance. We should keep information about our users, yes, but we don’t need to keep them forever. After a certain amount of time, that we should decide on, we should remove the session data from Redis. We can reinitiate the session data again when the user returns to our app.

Redis has a feature called Time To Live (TTL), that we can use to set the expiration time on sessions. This helps us clean up unused sessions and reduce memory usage. You can configure different expiration strategies like absolute or sliding expiration.

  • Absolute expiration: The session will expire after the specified time.
1
2
3
4
5
6
7
8
// Absolute expiration - the session will expire after the specified time
app.use(session({
  store: new RedisStore({ 
    client: redisClient,
    ttl: 86400 // 24 hours in seconds
  }),
  // ... other options
}));
  • Sliding expiration: The session again will expire after the specified time, but if the user is active, the counter will be reset on this version.
1
2
3
4
5
// Sliding expiration - resets the timer on each request
app.use((req, res, next) => {
  req.session.touch(); // Reset the session timer
  next();
});

Scaling Your Session Management

You might think like “okay, we could have built what we have seen so far with the default memory store, with some extra functions”. You are right, but the real power of Redis starts to shine when you start scaling your application horizontally.

Here’s how a typical scaled architecture looks:

  1. You will set up multiple instances of your application, because you don’t want to have a single point of failure, or if something happens to one of your instances, you don’t want to have a downtime. But if you have implemented the session with the default memory store, you will not be able to share the session data between your application instances.
  2. You will set up a Redis server, and update your code to use Redis as a session store.
  3. You will set up a load balancer in front of your application. And with the help of this, you will start distributing your users’ traffic between your application instances.

With this setup, a user can hit any instances of your application and still have the same experience as if they were hitting a single instance.

Security Considerations

Sessions are vulnerable to attacks, so make sure to implement these security best practices:

1
2
3
4
5
6
7
8
9
app.use(session({
  // ... other options
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true, // This will prevent JavaScript access to the cookie
    sameSite: 'strict', // Protects against CSRF attacks
    maxAge: 24 * 60 * 60 * 1000
  }
}));

You should also consider:

  • A way to regenerate/invalidate sessions every time a user logs in.
  • IP tracking and rate limiting system to detect suspicious activities like multiple attempts to login.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
app.post('/login', (req, res) => {
  // Verify credentials...
  
  // If credentials are valid, regenerate session
  const userInfo = { id: 123, username: 'example_user' };
  
  req.session.regenerate((err) => {
    if (err) {
      return res.status(500).send('Session error');
    }
    
    // Store user info in the new session
    req.session.user = userInfo;
    req.session.authenticated = true;
    
    res.redirect('/dashboard');
  });
});

Handling Redis Connection Issues

Network issues can occur anytime, always try to implement proper error handling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
redisClient.on('error', (err) => {
  console.error('Redis error:', err);
  
  // Depending on your application needs, you might:
  // 1. Continue with in-memory sessions as fallback
  // 2. Fail properly with a message to users
  // 3. Attempt reconnection
});

// Implement reconnection strategy
function connectWithRetry() {
  redisClient.connect().catch(err => {
    console.error('Failed to connect to Redis. Retrying in 5 seconds...');
    setTimeout(connectWithRetry, 5000);
  });
}

connectWithRetry();

Session Storage in Clustered Environments

When your app has a large number of users, you might need to configure a Redis Cluster instead of a single Redis instance, because a single instance might not be able to handle your traffic efficiently.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { createCluster } = require('redis');

const redisCluster = createCluster({
  rootNodes: [
    {
      url: 'redis://redis-node1:6379'
    },
    {
      url: 'redis://redis-node2:6379'
    },
    {
      url: 'redis://redis-node3:6379'
    }
  ]
});

redisCluster.connect();

// Then use the cluster with your session store
app.use(session({
  store: new RedisStore({ client: redisCluster }),
  // ... other options
}));

Conclusion

Session management is a crucial part of building scalable applications. Redis is your go to solution for session management in production environments, because of its speed, persistence and shared access.

Next time when you start a new project, set up Redis for session management from the beginning. It might seem like an unnecessary expense at first, but it is a small investment that pays back as your application grows.