Amblem
Furkan Baytekin

Understanding HTTP 304, ETag, Cache-Control, and Last-Modified with Go

Master HTTP caching in Go with ETags and Cache-Control headers

Understanding HTTP 304, ETag, Cache-Control, and Last-Modified with Go
226
7 minutes

In the world of web development, optimizing performance is critical for delivering fast and efficient user experiences. HTTP caching mechanisms like HTTP 304 Not Modified, ETag, Cache-Control, and Last-Modified play a pivotal role in reducing server load and improving page load times. In this article, we’ll explore these concepts and demonstrate how to implement them in Go with a simple example.


What is HTTP 304 Not Modified?

The HTTP 304 Not Modified status code is part of the HTTP caching mechanism. It tells the client (e.g., a browser) that the requested resource hasn’t changed since the last time it was fetched. Instead of sending the full resource again, the server responds with a 304 status, allowing the client to use its cached version.

This saves bandwidth and speeds up page loads, especially for static assets like images, CSS, or JavaScript files.


Key Caching Concepts

ETag (Entity Tag)

An ETag is a unique identifier assigned to a specific version of a resource. It’s sent in the HTTP response header (ETag) and used by the client in subsequent requests via the If-None-Match header. If the ETag matches the server’s current resource, the server responds with a 304 status, indicating the resource hasn’t changed.

Cache-Control

The Cache-Control header defines how a resource should be cached, how long it should be cached, and who can cache it (e.g., browsers, CDNs). Common directives include:

Last-Modified

The Last-Modified header indicates the timestamp of the resource’s last modification. The client includes this timestamp in the If-Modified-Since header in subsequent requests. If the resource hasn’t been modified since that time, the server returns a 304 status.


How These Work Together

Here’s a typical flow:

  1. The client requests a resource.
  2. The server responds with the resource, including ETag, Cache-Control, and Last-Modified headers.
  3. On the next request, the client sends If-None-Match (with the ETag) and If-Modified-Since (with the Last-Modified timestamp).
  4. The server checks if the resource has changed:

    • If unchanged, it responds with 304 Not Modified.
    • If changed, it sends the updated resource with new headers.

This process ensures efficient use of cached resources, reducing unnecessary data transfers.


Implementing HTTP 304 and Caching in Go

Let’s build a simple Go HTTP server that implements ETag, Cache-Control, and Last-Modified to support HTTP 304 responses.

Example Code

Below is a Go program that serves a static file (example.txt) and implements caching headers.

go
package main import ( "crypto/md5" "fmt" "io" "log" "net/http" "os" "sync" "time" ) const ( filePath = "example.txt" maxInMemorySize = 2 * 1024 * 1024 // 2MB ) type CachedFile struct { Content []byte ETag string LastModified time.Time UseCache bool Path string Size int64 mu sync.RWMutex } func main() { cf, err := loadFile(filePath) if err != nil { log.Fatal(err) } http.HandleFunc("/resource", serveResource(cf)) log.Println("Serving on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } func loadFile(path string) (*CachedFile, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() stat, err := file.Stat() if err != nil { return nil, err } cf := &CachedFile{ LastModified: stat.ModTime().Truncate(time.Second), Path: path, Size: stat.Size(), } if stat.Size() <= maxInMemorySize { data, err := io.ReadAll(file) if err != nil { return nil, err } cf.Content = data hash := md5.Sum(data) cf.ETag = fmt.Sprintf(`"%x"`, hash) cf.UseCache = true } else { cf.ETag = fmt.Sprintf(`W/"%x-%x"`, stat.ModTime().Unix(), stat.Size()) cf.UseCache = false } return cf, nil } func serveResource(cf *CachedFile) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cf.mu.RLock() etag := cf.ETag lastMod := cf.LastModified cached := cf.UseCache content := cf.Content path := cf.Path cf.mu.RUnlock() if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("ETag", etag) w.Header().Set("Last-Modified", lastMod.Format(http.TimeFormat)) w.Header().Set("Cache-Control", "max-age=3600, public") w.Header().Set("Content-Type", "text/plain") if cached { _, err := w.Write(content) if err != nil { http.Error(w, "Error writing response", http.StatusInternalServerError) } } else { file, err := os.Open(path) if err != nil { http.Error(w, "Error opening file", http.StatusInternalServerError) return } defer file.Close() _, err = io.Copy(w, file) if err != nil { http.Error(w, "Error streaming file", http.StatusInternalServerError) } } } }

How It Works

  1. File Metadata Loading On startup, the server reads the file’s size and last modified timestamp.
  2. Smart Caching Strategy If the file size is less than or equal to 2 MB, it’s loaded into memory for faster access.
  3. ETag Generation

    • For small files: a strong ETag is generated using an MD5 hash of the content.
    • For large files: a weak ETag is created based on the file’s size and last modified time.
  4. Client Cache Validation Incoming requests are checked for the If-None-Match header. If the ETag matches, a 304 Not Modified response is returned.
  5. Content Delivery

    • If cached: content is served directly from memory.
    • If not cached: the file is streamed from disk on each request.

Testing the Server

  1. Create a file named example.txt with some content (e.g., “Hello, World!”).
  2. Run the Go program: go run server.go.
  3. Use a tool like curl or a browser to test:
bash
curl -i http://localhost:8080/resource -v # Output * Host localhost:8080 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:8080... * Connected to localhost (::1) port 8080 > GET /resource HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.7.1 > Accept: */* > * Request completely sent off < HTTP/1.1 200 OK HTTP/1.1 200 OK < Cache-Control: max-age=3600, public Cache-Control: max-age=3600, public < Content-Type: text/plain Content-Type: text/plain < Etag: 8ddd8be4b179a529afa5f2ffae4b9858 Etag: 8ddd8be4b179a529afa5f2ffae4b9858 < Last-Modified: Fri, 16 May 2025 10:17:57 GMT Last-Modified: Fri, 16 May 2025 10:17:57 GMT < Date: Fri, 16 May 2025 07:20:36 GMT Date: Fri, 16 May 2025 07:20:36 GMT < Content-Length: 13 Content-Length: 13 < Hello World! * Connection #0 to host localhost left intact
  1. Make a second request with the If-None-Match and If-Modified-Since headers (browsers do this automatically):
bash
curl -i -H "If-None-Match: <etag-from-previous-response>" -H "If-Modified-Since: <last-modified-from-previous-response>" http://localhost:8080/resource # Example curl -i -H "If-None-Match: 8ddd8be4b179a529afa5f2ffae4b9858" -H "If-Modified-Since: Fri, 16 May 2025 10:17:57 GMT" http://localhost:8080/resource # Output HTTP/1.1 304 Not Modified Date: Fri, 16 May 2025 07:26:37 GMT

If the file hasn’t changed, you’ll see a 304 Not Modified response.


SEO and Performance Benefits

Implementing HTTP 304 and caching headers offers several benefits:


Best Practices

  1. Use Strong ETags: Generate ETags based on content (e.g., MD5 hash) on small files for accuracy.
  2. Use Weak ETags: For large files, use a weak ETag based on size and modTime.
  3. Use Memory Cache: For small files, use a memory cache to serve requests faster.
  4. Set Appropriate Cache-Control: Tailor max-age and other directives to your content’s update frequency.
  5. Combine with Other Optimizations: Use compression (e.g., Gzip) and CDNs alongside caching.
  6. Test Thoroughly: Use tools like Lighthouse or WebPageTest to ensure your caching strategy is effective.

Conclusion

HTTP 304, ETag, Cache-Control, and Last-Modified are powerful tools for optimizing web performance. By implementing these in Go, you can create efficient, scalable web servers that deliver fast and responsive experiences. The example above demonstrates a simple yet effective way to get started with caching in Go.

Try experimenting with different Cache-Control directives or integrating this into a larger Go application. Happy coding!


Album of the day:

Suggested Blog Posts