Caching API Responses: Boosting Performance with HTTP Caching and 304 Status Codes

Caching API Responses: Boosting Performance with HTTP Caching and 304 Status Codes

Web applications often rely on API calls to fetch and display dynamic data, such as product reviews, customer feedback, or pricing information. However, repeatedly fetching the same data for unchanged content can lead to unnecessary network latency and increased server load. To optimize performance, we can leverage HTTP caching mechanisms, similar to how browsers cache images and JavaScript/CSS files.

By using the Cache-Control header and ETags, we can instruct browsers to cache API responses effectively. When a client makes a subsequent request for the same resource, the server can respond with a 304 Not Modified status code if the content hasn't changed. This tells the browser to use its cached copy, reducing data transfer and improving loading times.

In this article, we'll explore how to implement API response caching using Cache-Control headers, ETags, and conditional requests.


Let's begin by examining a scenario without caching.

This Node.js server serves static files, index.html, from the "public" folder and provides a /getBook API endpoint to retrieve book information. Note: I have disabled etag automatic generation for the demonstration in this article.

//server.js

const fs = require('fs');
const books = JSON.parse(fs.readFileSync("./data.json"));

const express = require('express');
const app = express();

app.set('etag', false);
app.use(express.static('public'));     //we serve index.html under public folder

app.get('/', (req, res, next) => {
  res.status(200).send();
});

app.get('/getBook', (req, res) => {
  console.log('Finding book: ' + req.query.id)

  const data = books[req.query.id]
  res.status(200).send(data);
});

app.listen(3000, () => {
  console.log('Backend server listening on port 3000');
});        
<!-- ./public/index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Fetch Example</title>
</head>
<body>
  <input type="text" id="idInput" placeholder="Enter ID">
  <button id="getDataButton">Get Data</button>
  <div>
      <h2 id="title"></h2>
      <span id="longDescription"></span>
  </div>
  
  <script>
    const idInput = document.getElementById('idInput');
    const getDataButton = document.getElementById('getDataButton');
    const title = document.getElementById('title');
    const longDescription = document.getElementById('longDescription');
  
    getDataButton.addEventListener('click', () => {
      const id = idInput.value;
      fetch(`/getBook?id=${id}`)
        .then(response => response.json())
        .then(data => { 
          title.textContent = `Title: ${data.title}`;
          longDescription.textContent = data.longDescription;
        })
        .catch(error => {
          resultDiv.textContent = 'Error fetching data';
        });
    });
  </script>
</body>
</html>        

To illustrate the issue, I clicked the "Get Data" button three times with the same ID. As you can see in the network tab, each click resulted in a separate API request to the server, even though the response data remained identical. This repetitive fetching leads to redundant network activity and increased latency, as evidenced by the consistent 200 OK responses. However, we can significantly optimize this behavior by implementing caching techniques.

Article content

To optimize our API calls, let's enable caching and use ETags. This technique allows the server to return a 304 Not Modified status code (without sending the response body) if the content hasn't changed since the client's last request. This approach significantly reduces data transfer and improves loading times.

The code bellow demonstrates how to implement ETag-based caching in an Express.js server. It generates an ETag for the response data and compares it with the If-None-Match header sent by the client. If the ETags match, the server sends a 304 response, signaling to the client that it can use its cached copy. Otherwise, it sends the full response with the new ETag.

app.get('/getBook', (req, res) => {
  console.log('Finding book: ' + req.query.id)

  const data = books[req.query.id]
  const etag = generateEtag(data);
  
  if (req.headers['if-none-match'] === etag) {
    res.status(304).send();
  } else {
    //Set cache-control header
    res.setHeader('Cache-Control', 'public, max-age=3600'); 
  
    //Set etag header
    res.setHeader('ETag', etag);
    res.status(200).send(data);
  }
});        


When we first request data, the server responds with a 200 OK status and a 4.5 KB response body. This response includes Cache-Control and ETag headers, instructing the browser to cache the data. Subsequent requests then efficiently load the data from the browser's disk cache.


Article content

While this caching mechanism effectively reduces data transfer, it doesn't fully address my requirements. Ideally, I want the browser to always make API calls to the server, even if the data is cached, for two primary reasons:

  1. Data Validation: I need to ensure that the response hasn't changed on the server, even if the cached version hasn't expired.
  2. API Call Analytics: I want to track and analyze the number of API calls on the server-side for monitoring and optimization purposes.

To achieve this, I'll modify the caching strategy to allow the browser to make requests while still leveraging cached data when possible. The server will then determine whether to return the full data with a 200 OK status or signal that the data is unchanged with a 304 Not Modified status. We can modify our caching strategy on either the server-side or the client-side:

  1. Server-side: Include the no-cache directive in the Cache-Control header of the response. This instructs the browser to revalidate the cached response with the server on each request.
  2. Client-side: Provide the cache: 'no-cache' option in the fetch API call. This tells the browser to make a conditional request to the server, even if a cached response is available.

//server.js
...
app.get('/getBook', (req, res) => {
 ...
  if (req.headers['if-none-match'] === etag) {
    res.status(304).send();
  } else {

    // Set no-cache here
    res.setHeader('Cache-Control', 'public, max-age=3600, no-cache');
...
  }
});        
// client-side code
  <script>
    const idInput = document.getElementById('idInput');
     ...
  
    getDataButton.addEventListener('click', () => {
      const id = idInput.value;
      
      //Set cache option
      fetch(`/getBook?id=${id}`, { cache: 'no-cache' })
       ....
    });

  </script>        

After modifying the code, observe the network tab. The first request still results in a 200 OK status with the full response data. However, subsequent requests now trigger a server check, but instead of returning the full data again, the server responds with a 304 Not Modified status, indicating that the content hasn't changed. This efficiently avoids unnecessary data transfer while still allowing us to track API calls on the server.

Article content

Summary:

By implementing HTTP caching with ETags and Cache-Control headers, we gain several significant benefits:

  • Reduced server load: The server avoids sending the full response repeatedly when the data hasn't changed, minimizing unnecessary data transfer and processing.
  • Improved performance: Clients receive data faster as they can utilize cached responses, leading to quicker page loads and a better user experience.
  • Lower bandwidth consumption: Reduced data transfer translates to lower bandwidth usage for both the client and the server, which can be especially important for mobile users or those with limited data plans.
  • Enhanced scalability: Caching helps improve the scalability of your application by reducing the load on the server, allowing it to handle more requests efficiently.

Overall, HTTP caching is a valuable technique for optimizing web applications, leading to faster loading times, reduced server load, and a smoother user experience.

To view or add a comment, sign in

More articles by Sanhapon Thadapradit

  • How I Doubled My PRs with an AI Assistant

    Today, I want to share my experience with a coding workflow that combines AI with spec-driven development. I've used…

    1 Comment
  • Nginx JavaScript (NJS): Using Node Modules

    NJS lets you write JavaScript code that runs directly within Nginx. It's designed for speed and efficiency, offering a…

  • Nginx Javascript Module

    A few years ago, at my previous company, we were exploring a new tech stack for our website. To evaluate its…

Others also viewed

Explore content categories