Back to Blog
API ArchitectureJuly 11, 20264 min read

REST API Caching: ETag vs Last-Modified Headers Explained

Master REST API caching by using ETag and Last-Modified headers. Learn the difference, when to use each, and how to optimize your API performance today.

REST APICachingHTTPPerformanceAPI DesignETagBackendAPIREST

When you're scaling an API, the most expensive request is the one you don't actually need to process. If a client already has the latest version of a resource, sending that data again is a waste of CPU, database I/O, and bandwidth.

I’ve spent plenty of time debugging latency spikes, only to realize we were serving the same static configurations or unchanged user profiles thousands of times per minute. Implementing proper REST API caching isn't just about speed; it's about building a robust foundation for your services.

Understanding ETag vs Last-Modified

At the core of API performance optimization are two HTTP headers: ETag and Last-Modified. While they serve a similar purpose—letting a client know if they can safely use their cached copy—they work in fundamentally different ways.

HeaderTypeBest Used ForComparison Method
Last-ModifiedTimestampSimple, static, or rarely updated dataDate comparison
ETagHash/TokenDynamic content or complex objectsEquality check

Last-Modified is straightforward. The server sends a date, and the client sends it back later in an If-Modified-Since header. If the server’s file hasn’t changed, it returns a 304 Not Modified status with an empty body. It's fast, but it relies on system clocks and has a resolution limit of one second.

ETag (Entity Tag) is more precise. It’s an opaque identifier, usually a hash of the response body. If the data changes by even one byte, the hash changes. The client sends this back in an If-None-Match header. It’s the gold standard for REST API design best practices when you need accuracy.

Implementing ETag in Production

I once spent about two days refactoring a legacy endpoint that was hammering our database for every request. We switched to ETags, and the result was immediate: we cut redundant payload transfers by roughly 60%.

Here is a simplified workflow for handling an ETag:

  1. Generate: Calculate a hash of your response body (e.g., md5 or sha1).
  2. Send: Include the ETag: "hash-value" header in your response.
  3. Validate: On the next request, check the If-None-Match header from the client.
  4. Respond: If the hashes match, return 304 Not Modified. Otherwise, return the full resource with a 200 OK.
PYTHON
# Conceptual example of ETag validation
import hashlib

def get_resource(resource_id):
    data = fetch_from_db(resource_id)
    # Create an ETag based on data content
    etag = hashlib.md5(str(data).encode()).hexdigest()
    
    return {
        "data": data,
        "headers": {"ETag": etag}
    }

# In your API middleware/controller:
if request.headers.get("If-None-Match") == current_etag:
    return Response(status=304)

Common Trade-offs

We first tried generating ETags on the fly for every single request. It broke because the hashing process itself was CPU-intensive, effectively shifting our bottleneck from the database to the application server.

Instead, we moved the ETag generation to the database layer or used a cached version of the object. If you're building a Laravel REST API development project, look into using model events or middleware to manage these headers efficiently.

When to Avoid Caching

Don't over-engineer. If your resource changes every time it's requested, caching headers are useless and will only add overhead. Always consider the nature of your data:

  • Highly dynamic data: Skip ETags.
  • Public data: Use Cache-Control: public, max-age=... alongside ETags.
  • Private/User-specific data: Always use private to avoid caching sensitive data in intermediate proxies.

If you're dealing with complex data structures, check out my thoughts on REST API Field Selection: Solving Data Over-fetching and Dependency Graphs to see how reducing payload size complements your caching strategy.

Frequently Asked Questions

Can I use ETag and Last-Modified together? Yes, and you should. If the client sends both If-None-Match and If-Modified-Since, most servers prioritize the ETag because it’s more specific.

Is ETag generation slow? It can be if you hash a massive response body. For large payloads, consider using a version identifier or a "last updated" timestamp from your database record instead of hashing the entire body.

What happens if my ETag is wrong? If your ETag generation logic is flawed (e.g., it doesn't account for a field that should trigger a cache bust), the client will keep seeing stale data. Always verify your cache-busting logic in your integration tests.

Final Thoughts

Using HTTP cache headers is one of the easiest ways to improve your API's perceived performance. Start small—implement Last-Modified on your most stable endpoints, and move to ETag when you need that extra bit of precision. I'm still experimenting with caching strategies for agentic API consumption, where stale data can cause hallucinations in AI agents, but for standard RESTful services, these headers remain the gold standard.

Similar Posts