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:
-
max-age=<seconds>
: Specifies how long the resource is considered fresh. -
no-cache
: Forces the client to validate with the server before using the cached version. -
public
orprivate
: Indicates whether the resource can be cached by intermediaries (e.g., CDNs).
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:
- The client requests a resource.
-
The server responds with the resource, including
ETag
,Cache-Control
, andLast-Modified
headers. -
On the next request, the client sends
If-None-Match
(with the ETag) andIf-Modified-Since
(with the Last-Modified timestamp). -
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.
gopackage 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
- File Metadata Loading On startup, the server reads the file’s size and last modified timestamp.
- Smart Caching Strategy If the file size is less than or equal to 2 MB, it’s loaded into memory for faster access.
-
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.
-
Client Cache Validation
Incoming requests are checked for the
If-None-Match
header. If the ETag matches, a304 Not Modified
response is returned. -
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
-
Create a file named
example.txt
with some content (e.g., “Hello, World!”). -
Run the Go program:
go run server.go
. -
Use a tool like
curl
or a browser to test:
bashcurl -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
-
Make a second request with the
If-None-Match
andIf-Modified-Since
headers (browsers do this automatically):
bashcurl -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:
- Improved Page Load Speed: Cached resources reduce server requests and data transfers.
- Reduced Server Load: Fewer full responses mean less strain on your server.
- Better SEO: Search engines like Google prioritize fast-loading websites, and caching helps achieve this.
- Enhanced User Experience: Faster load times lead to happier users and lower bounce rates.
Best Practices
- Use Strong ETags: Generate ETags based on content (e.g., MD5 hash) on small files for accuracy.
- Use Weak ETags: For large files, use a weak ETag based on size and modTime.
- Use Memory Cache: For small files, use a memory cache to serve requests faster.
-
Set Appropriate Cache-Control: Tailor
max-age
and other directives to your content’s update frequency. - Combine with Other Optimizations: Use compression (e.g., Gzip) and CDNs alongside caching.
- 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: