← Back to list

What is ETag in HTTP? Stop Sending Unchanged Data Every Time!

ETag (entity tag) is an identifier for a specific version of a resource. An easy way to understand it is like how every software has a…

Minhajul Islam (Minhaj) · 2025-12-25 14:20 · 6 claps · 5.9 min read
#etag #https #optimization #software-development #cache
Open on Medium ↗

What is ETag in HTTP? Stop Sending Unchanged Data Every Time!

ETag (entity tag) is an identifier for a specific version of a resource. An easy way to understand it is like how every software has a version number — that’s similar to what ETag does for your data.

When and Why Do We Use ETag?

Imagine you have a ton of data in an endpoint like “/api/v1/todos”. Every time someone hits that endpoint, the server has to send out around 5MB of data.

Do you think serving that much data over and over isn’t expensive?

It chews up bandwidth, ramps up latency, and puts a lot of stress on the server, right?

That’s where ETag comes in handy — it helps avoid sending the full 5MB every single time.

Here’s how it works: On the client’s first request to “/api/v1/todos”, the server sends the full data along with a header like ETag: “some-value”. What is this value? It’s often a hash (like ***SHA-256***) of the data: The server pulls the data from the database, hashes it, and sets that as the ETag.

The client (like your frontend code) receives the response, saves the data in local storage or cache, and also keeps the ETag. Next time the client requests the same endpoint, it includes the ETag in its request (using the “If-None-Match” header).

The server then grabs the current data from the database, computes its hash to get a new ETag, and compares it to the one the client sent. If they match (meaning the data hasn’t changed), the server just responds with a 304 “Not Modified” status code.

No need to send the huge 5MB payload — just a quick “Hey, nothing’s changed; use what you’ve got in your cache!”

If the data has changed, the server’s new ETag won’t match the client’s. In that case, the server sends the updated full body with the new ETag.

(You know, every HTTP response has two main parts: the headers and the body — the body is where the actual data lives.)

The client will also get the new ETag and update both the cached data and the ETag!

That’s the core idea behind ETag.

But you might be wondering, “Wait, what’s the big win? The client still hits the server every time, and the server still queries the database, computes the hash, compares, and responds — sounds like the same effort, right?”

The real gains are in reducing latency (quicker responses) and saving bandwidth (no big data transfers when nothing’s changed). For large datasets, that adds up fast!

Let’s implement, guys:

Note: I’m gonna use NestJS and ReactJS for showing the code, okay?

backend code

import * as crypto from 'crypto';
import type { Request, Response } from 'express';
import {Controller, Get, Req, Res} from '@nestjs/common';

@Controller('api/v1/todos')
export class TodosController {

 constructor(private readonly todosService: TodosService) {}

 @Get()
 async findAll(@Req() req: Request, @Res() res: Response) {

  const data = await this.todosService.findAll();

  const body = JSON.stringify(data);

  const etag = crypto.createHash('md5').update(body).digest('hex');

  const clientEtag = req.headers['if-none-match'];

  if (clientEtag && clientEtag === etag) {

     res.status(304).end();
     return;
  }

  res.set('ETag', etag);
  return res.json(data);
 }
}

As you can see, the backend code is straightforward if you’re familiar with a bit of TypeScript.

When a client requests the data, the server checks for an ETag in the request headers (sent via “If-None-Match”). If present, it compares this against a newly computed ETag using Node.js’s built-in crypto library.

This uses an MD5 hash (similar to ***SHA-256***), which always generates the same output for identical inputs but changes if the input differs.

First request (no ETag from client): The server fetches the todo list from the database, stringifies it to JSON, computes an MD5 hash to create the ETag, sets the ETag header in the response, and sends the full data.

Subsequent requests: If the client sends the previous ETag, the server fetches the current todo list from the database, computes its ETag, and compares the two. If they match (meaning the data hasn’t changed), it returns a 304 Not Modified status without any data body, saving bandwidth. If they differ (due to any updates), it sends the updated data along with the new ETag.

frontend code

export const TodosPage = () => {

 const [isLoading, setIsLoading] = useState(false);
 const [todos, setTodos] = useState<Array<Todo>>([]);

 const getDataFn = async () => {
  setIsLoading(true);

  const lastETag = localStorage.getItem("todosETag");

  const response = await fetch(BASE_URL, {
   headers: lastETag ? { "If-None-Match": lastETag } : {}, // see this
  });

  const etag = response.headers.get("ETag");

  if (response.status === 304) {

   const cachedData = localStorage.getItem("todosData");

   if (cachedData) {
    setTodos(JSON.parse(cachedData));
   }
   setIsLoading(false);
   return;
  }

  if (!response.ok) {

  console.error("Fetch error:", response.status);

  setIsLoading(false);

  return;
 }

 const data = await response.json();

 localStorage.setItem("todosData", JSON.stringify(data));

 if (etag) {
  localStorage.setItem("todosETag", etag);

 }

 setIsLoading(false);
 setTodos(data);
 };

 useEffect(() => { getDataFn() }, []);

 return (
  // JSX RENDER
  // FORM FOR TODO CREATE
  // RNEDER THE TODO LIST
 )
}

As you can see, the frontend code is straightforward if you’re familiar with React and TypeScript.

This React component fetches and displays a todo list, handling ETag caching to avoid unnecessary data reloads. It uses state for loading and the todos array, and a single async function (getDataFn) to handle the fetch, called once on component mount via useEffect.

It checks localStorage for a previous ETag and sends it in the request headers as “If-None-Match” (if available). After fetching, it grabs the new ETag from the response headers.

If the server returns 304 (data unchanged), it loads the cached todos from localStorage and updates the state — no new data is fetched.

Otherwise, it parses the JSON response, stores the updated todos and ETag in localStorage for future use, and updates the state to render the list.

The rest is just JSX for rendering the todos (with a form to create new ones, not shown in detail here).

Mainly, this is the concept here, but I implemented it manually to understand it better. See some pieces of images for illustration:

On the first request, the simple frontend doesn’t send an ETag via the “If-None-Match” header. It gets the data from the backend, and the backend sends the data while setting the ETag in the response header. The frontend catches the data and ETag, then stores them in localStorage.

On the next request, the frontend sends the ETag along with the request. The backend receives the ETag from the request header, fetches the full data from the database, generates a new ETag, and compares it with the one sent by the frontend. If they match, it returns a 304 Not Modified response.

As you can see, the response shows “Not Modified” with status code 304.

Here, I’ve created a new todo and refetched. The frontend sends the old ETag, but since the data changed, the frontend’s ETag and the backend’s newly generated ETag don’t match. That’s why the backend responds with the updated data and sets a new ETag.

Next, when I refresh multiple times (without changes), you’ll see the “Not Modified” response again. As you can see, the backend doesn’t send the body anymore — it just returns 304 to save bandwidth.

Hey, in nowadays frameworks, they manage ETags by default. For example, if you’re working with Node.js’s Express.js framework, it handles ETags automatically.

When you don’t handle it manually, the browser will do it by default: it’ll cache things and manage everything on its own.

But understanding the concept better can help with any framework you use — no matter which one, the idea is the same.

Conclusion

As you can see, the main thing is the concept — it’s super handy for optimization, right? I hope you guys have a clear understanding of the ETag concept now!

I’m also trying to learn, implement, and write down topics like this!

I could be mistaken; just hit the ***LinkedIn or [Email](http://minhajul.minhaj.islam@gmail.com/)***.


메타데이터
post_id
e5db4aa0d443
slug
what-is-etag-in-http-stop-sending-unchanged-data-every-time-e5db4aa0d443
url
https://medium.com/@minhajul-im/what-is-etag-in-http-stop-sending-unchanged-data-every-time-e5db4aa0d443
canonical_url
https://medium.com/@minhajul-im/what-is-etag-in-http-stop-sending-unchanged-data-every-time-e5db4aa0d443
author_url
https://medium.com/@minhajul-im
status
ok
fetched_at
2026-07-13 22:58:47