Skip to content

ExpressJS - Slowing Down Responses

Slowing down responses is an advanced rate-limiting technique. Instead of immediately rejecting requests that exceed a certain limit, you can introduce an artificial delay. This makes it significantly harder and more expensive for an attacker to conduct brute-force attacks (like password guessing) or scrape your API, as their scripts will be slowed to a crawl.

The express-slow-down middleware is a perfect tool for this job. It’s often used in conjunction with a standard rate limiter (express-rate-limit).

Install the package using npm.

npm install express-slow-down

Let’s create a speed limiter that applies to a sensitive endpoint, like a login route. We’ll allow a few requests at full speed and then start adding a delay to subsequent requests.

import express from 'express';
import slowDown from 'express-slow-down';
const app = express();
// Configure the speed limiter
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 5, // Allow 5 requests per 15 minutes to go at full speed
delayMs: (hits) => hits * 100 // Add 100ms of delay for each request after the 5th one
// (6th request = 100ms, 7th = 200ms, etc.)
});
// Apply the middleware to a specific, sensitive route
app.post('/login', speedLimiter, (req, res) => {
// Your login logic here...
res.send('Login attempt processed.');
});
app.get('/', (req, res) => {
res.send('This page is not slowed down.');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});

You can test this using an API client like Postman or curl in your terminal. For the first 5 POST requests to /login, the response will be immediate. You can observe this in your browser’s Developer Tools in the ‘Network’ tab, looking at the ‘Time’ column.

On the 6th request, you will notice a delay of about 100ms. The 7th will be delayed by ~200ms, and so on. This demonstrates the slowdown in action.

3. Advanced Configuration and Best Practices

Section titled “3. Advanced Configuration and Best Practices”

For a production environment, consider these advanced settings.

An attacker could deliberately make many requests to create an extremely long delay for themselves, which might not be desirable. You can cap the maximum delay.

const cappedSpeedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 5,
delayMs: (hits) => hits * 100,
maxDelayMs: 2000 // Cap the delay at 2 seconds
});

Just like express-rate-limit, the default in-memory store is not suitable for applications running on multiple servers or processes. If one user hits server A and then server B, their request counts will be separate. To enforce a consistent slowdown across your infrastructure, use a shared store like Redis. See the package documentation for store implementations.

When to Use Response Slowing vs. Rate Limiting

Section titled “When to Use Response Slowing vs. Rate Limiting”
  • Use Rate Limiting (express-rate-limit) for general protection of all your API endpoints. It’s a blunt instrument that rejects requests (with a 429 Too Many Requests status) to protect your server’s resources. It’s great for preventing general DoS attacks.
  • Use Response Slowing (express-slow-down) for specific, sensitive endpoints where an attacker might try to guess information or brute-force a login. It doesn’t reject the request but makes the attack economically unfeasible due to the time cost. It provides a better user experience for legitimate users who might accidentally hit a limit, as their request still gets processed, just with a slight delay.

Often, the best strategy is to use both: a general rate limit on all routes and a more specific slowdown on authentication or password reset endpoints.