DEV Community

Cover image for 5 Reasons Your Express App Is Running Slowly
Arunangshu Das
Arunangshu Das

Posted on

5 Reasons Your Express App Is Running Slowly

Express.js is the go-to web framework for Node.js developers—and for good reason. It's minimal, unopinionated, fast, and easy to learn. But once your app starts growing, you may begin noticing performance issues. Routes take longer to respond, memory usage climbs, and your once snappy API starts to feel sluggish.

If you've been scratching your head, wondering why your Express app isn’t performing as expected, you're not alone. Performance bottlenecks creep in silently and often stem from common pitfalls that developers—especially in fast-paced environments—don’t notice until the app hits production.

Reason 1: Blocking the Event Loop

The Problem:

Node.js is single-threaded and uses an event-driven model to handle concurrency. This means your entire application can grind to a halt if you accidentally write blocking (synchronous) code in any part of the request/response cycle.

Some common culprits include:

  • CPU-intensive operations (e.g., image processing, complex calculations)
  • Synchronous file system access
  • Poorly optimized loops or recursive functions

If your route handler is performing heavy computation or waiting on a blocking function, it can freeze the entire server, not just the current request.

How to Identify:

Use the clinic.js tool (especially clinic doctor) to identify event loop lag.

npm install -g clinic
clinic doctor -- node app.js
Enter fullscreen mode Exit fullscreen mode

You can also use process.hrtime() or async_hooks to manually track lag.

The Fix:

  • Offload CPU-heavy tasks to a worker thread or external service.
  • Use fs.promises or asynchronous versions of APIs.
  • Consider bull + Redis for job queues.
  • Use child_process or worker_threads for isolated, heavy tasks.

Example:

Bad (Blocking)

app.get('/slow', (req, res) => {
  const data = fs.readFileSync('./large-file.txt'); // blocking
  res.send(data.toString());
});
Enter fullscreen mode Exit fullscreen mode

Good (Non-blocking)

app.get('/fast', async (req, res) => {
  const data = await fs.promises.readFile('./large-file.txt');
  res.send(data.toString());
});
Enter fullscreen mode Exit fullscreen mode

Reason 2: Unoptimized Middleware Stack

The Problem:

Middleware in Express is executed in order—one by one. If you load too many unnecessary or poorly optimized middlewares on every request, you’re bound to slow things down.

Even worse, some middleware (like body-parser) may parse large bodies even when they’re not needed, wasting precious CPU time.

How to Identify:

Log middleware execution times using a custom middleware or profiling tool like morgan, express-status-monitor, or clinic.

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.url} took ${Date.now() - start}ms`);
  });
  next();
});
Enter fullscreen mode Exit fullscreen mode

The Fix:

  • Apply middleware only where necessary using route-level usage.
  • Avoid using heavy middleware globally unless it’s essential.
  • Use compression and helmet wisely—turn them off in dev mode.

Example:

Bad: Global middleware for everything

app.use(bodyParser.json()); // applied even on GET requests
app.use(cors());
Enter fullscreen mode Exit fullscreen mode

Good: Scoped middleware

app.get('/health', (req, res) => res.send('OK')); // no middleware needed here
 
app.post('/data', bodyParser.json(), (req, res) => {
  // only parse body here
});
Enter fullscreen mode Exit fullscreen mode

Reason 3: Slow or Inefficient Database Queries

The Problem:

Even the most optimized Express app will crawl if your database calls are slow or unindexed. Common issues include:

  • N+1 queries
  • Missing indexes
  • Inefficient joins
  • Fetching too much data (no pagination or projections)

Since database calls are typically I/O-bound, poor performance here creates a domino effect.

How to Identify:

  • Use tools like Mongoose Debug, Sequelize logging, or query profilers.
  • Log query times manually.
mongoose.set('debug', true);
Enter fullscreen mode Exit fullscreen mode

The Fix:

  • Use Indexes: Ensure that frequently queried fields are indexed.
  • Paginate: Always paginate when dealing with large datasets.
  • Optimize Queries: Fetch only required fields using projection/select.

Example:

Bad: Fetching all fields without pagination

const users = await User.find(); // no limit or select
Enter fullscreen mode Exit fullscreen mode

Good: Paginated and projected

const users = await User.find({}, 'name email').limit(50).skip(100);
Enter fullscreen mode Exit fullscreen mode

Bonus:

If you're using MongoDB, use the Aggregation Pipeline smartly to do in-DB transformations and avoid bloating your Node.js server with processing logic.

Reason 4: No Caching Layer

The Problem:

If you're hitting your database or third-party APIs on every single request, you're wasting resources and introducing latency.

Even static content like config, user sessions, or metadata can and should be cached.

How to Identify:

  • Notice repeated DB/API calls for the same data in a short time.
  • Monitor request durations for static pages or repeat visits.

The Fix:

  • In-Memory Caching: Use node-cache or memory-cache for small apps.
  • Distributed Caching: Use Redis for multi-instance or clustered apps.
  • Cache Headers: Leverage HTTP caching (ETags, Cache-Control).

Example using Redis:

const redis = require('redis');
const client = redis.createClient();
 
app.get('/profile/:id', async (req, res) => {
  const id = req.params.id;
  client.get(id, async (err, cached) => {
    if (cached) return res.send(JSON.parse(cached));
 
    const user = await User.findById(id);
    client.setex(id, 3600, JSON.stringify(user)); // cache for 1 hour
    res.send(user);
  });
});
Enter fullscreen mode Exit fullscreen mode

Reason 5: Large Payloads and Uncompressed Responses

The Problem:

Transferring large payloads without compression can be brutal on both server and client—especially on mobile networks or when dealing with JSON-heavy APIs.

Even more so when users are sending large file uploads or form data without limits set.

How to Identify:

  • Look at network payload sizes via browser dev tools.
  • Use tools like Postman to inspect response headers and sizes.
  • Audit Express logs or middleware metrics.

The Fix:

  • Use compression middleware (gzip or brotli).
  • Limit payload size in body-parser or multer.
  • Avoid over-fetching (don’t send the entire user object if only email is needed).

Example:

const compression = require('compression');
app.use(compression()); // gzip all responses
 
app.use(bodyParser.json({ limit: '1mb' })); // prevent large payload attacks
Enter fullscreen mode Exit fullscreen mode

Also, when serving static files, precompress assets and serve .gz versions.

Bonus Tips for Speeding Up Express Apps

  1. Use Cluster Mode
       Leverage all CPU cores using PM2 or the native cluster module.

  2. Lazy Load Routes
       For large applications, split routes into smaller modules and load them only when needed.

  3. Avoid Memory Leaks
       Use heapdump, leakage, or memwatch to profile your app.

  4. Use CDN for Static Assets
       Offload images, JS, and CSS to a CDN instead of serving from the same server.

  5. Use async/await everywhere
       Keep your code non-blocking and easier to manage.

Final Thoughts

Slow Express apps are not usually caused by the framework itself—it’s almost always in how it’s used. When you're working with real users and real traffic, ignoring performance becomes expensive quickly.

With tools, logs, and conscious design patterns, these problems can be fixed—often with just a few lines of code.

→ Use async patterns
→ Optimize middleware
→ Streamline database access
→ Implement caching
→ Compress everything

You may also like:

  1. Top 10 Large Companies Using Node.js for Backend

  2. Why 85% of Developers Use Express.js Wrongly

  3. Top 10 Node.js Middleware for Efficient Coding

  4. 5 Key Differences: Worker Threads vs Child Processes in Node.js

  5. 5 Effective Caching Strategies for Node.js Applications

  6. 5 Mongoose Performance Mistakes That Slow Your App

  7. Building Your Own Mini Load Balancer in Node.js

  8. 7 Tips for Serverless Node.js API Deployment

  9. How to Host a Mongoose-Powered App on Fly.io

  10. The Real Reason Node.js Is So Fast

  11. 10 Must-Know Node.js Patterns for Application Growth

  12. How to Deploy a Dockerized Node.js App on Google Cloud Run

  13. Can Node.js Handle Millions of Users?

  14. How to Deploy a Node.js App on Vercel

  15. 6 Common Misconceptions About Node.js Event Loop

  16. 7 Common Garbage Collection Issues in Node.js

  17. How Do I Fix Performance Bottlenecks in Node.js?

  18. What Are the Advantages of Serverless Node.js Solutions?

  19. High-Traffic Node.js: Strategies for Success

Read more blogs from Here

You can easily reach me with a quick call right from here.

Share your experiences in the comments, and let's discuss how to tackle them!

Follow me on LinkedIn

Top comments (0)