Efficient HTTP caching

Simple, RESTful and effective

Aims

  • Why you might be interested
  • How proxies affect HTTP
  • How you can improve HTTP traffic
  • Why this is good

Audience

  • Front-end
  • Back-end
  • Standards wonks
  • Architects

Assumptions

HTTP requests

curl

curl -i, --include
     -H, --header

What's a request?

Client

GET /my-awesome-resource HTTP/1.1
Host: awesome-server

Server

HTTP/1.1 200 Fantastic
Content-Type: text/plain

No problem.

Actually...

  • User-Agent asks a proxy
  • Proxy asks an origin server

Actually actually...

  • JavaScript in a browser
  • ...to the browser's cache...
  • ...to a corporate proxy...
  • ...to a service's edge reverse proxy...
  • ...to a service front-end proxy...
  • ...to a Tomcat container

For our purposes

  • Dumb client
  • Caching proxy
  • Origin server

Dumb client

Always asks for a resource

Caching proxy

May, or may not, forward the request.

Motivating examples

Reporting tool

  • Frequent runs
  • Open-ended set of resources
  • Effectively unchanged data

Uncached

  • First run, slow
  • Second run, slow
  • Third run, slow

With long-lived caching

  • First run, slow
  • Second run, fast
  • Third run, fast

Only waiting for new resources

/config/cookie

Rarely changes (never changes in OnDemand).

Fetched every time.

Implement a cache?

Principle of REST - origin server gets to define resource behaviour

As the producer, think about the meaning of this resource.

HTTP/1.1 200 Okay
Content-Type: application/xml
Cache-Control: max-age=30

First request

Dumb client

GET /config/cookie HTTP/1.1
Host: horde

Caching proxy

GET /config/cookie HTTP/1.1
Host: horde

Origin server

HTTP/1.1 200 Okay
Content-Type: application/xml
Cache-Control: max-age=30

Second request

Dumb client

GET /config/cookie HTTP/1.1
Host: horde

Caching proxy

Responds from cache.

Bandwidth saved

More than 50ms saved from application's critical path

For free.

TODO

Conditional requests

Polling large, infrequently-changing resources.

Ask, but tell the server what we already have.

Caching proxy

GET /rest/usermanagement/1/group/membership HTTP/1.1
Host: horde

Origin server

HTTP/1.1 200 Okay
Content-Type: application/xml
Cache-Control: max-age=0
ETag: W/"2401adb2cffefdbfcd7f63f17b91a5d946e7855c"

<large-xml-document>
...

Caching proxy

GET /rest/usermanagement/1/group/membership HTTP/1.1
Host: horde
If-None-Match: W/"2401adb2cffefdbfcd7f63f17b91a5d946e7855c"

Origin server

HTTP/1.1 304 Not modified
Content-Type: application/xml
Cache-Control: max-age=0
ETag: W/"2401adb2cffefdbfcd7f63f17b91a5d946e7855c"

No response body.

If that version check is cheap, polling is free.

If that version check is cheap, polling is free.

Caching proxy

GET /rest/usermanagement/1/group/membership HTTP/1.1
Host: horde
If-None-Match: W/"2401adb2cffefdbfcd7f63f17b91a5d946e7855c"

Origin server

HTTP/1.1 200 Okay
Content-Type: application/xml
Cache-Control: max-age=0
ETag: W/"01f4a675194031d0056d2b43a90d91712fccdb37"

<large-xml-document>
...

With dates

  • Last-Modified:
  • Expires:
  • If-Modified-Since:
  • Only requesting when needed
  • Only requesting when stale
  • Only waiting for bandwidth of changed resources
  • Still blocking on validation

Origin server

HTTP/1.1 200 Okay
Content-Type: application/xml
Cache-Control: max-age=30, stale-while-revalidate=30
  • Only requesting when needed
  • Only requesting when stale
  • Only waiting for bandwidth of changed resources
  • Asynchronous validation

Implementation

Python

httplib2

In-process caching proxy

Not Requests

Python

requests + CacheControl

A copy of httplib2's algorithms into Requests.

Without

import requests

session = requests.Session()

response = session.get("http://www.example.com/")

With

import requests
from cachecontrol import CacheControl
from cachecontrol.caches import FileCache

uncachedSession = requests.Session()
session = CacheControl(uncachedSession, cache = FileCache('.cache'))

response = session.get("http://www.example.com/")

Being more specific

import requests
from cachecontrol import CacheControl
from cachecontrol.caches import FileCache

uncachedSession = requests.Session()
session = CacheControl(uncachedSession, cache = FileCache('.cache'))

response = session.get("http://www.example.com/",
             headers = {'Cache-Control': 'max-age=3600'})

Java

Apache HttpComponents HttpCache

Without


import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
...
        CloseableHttpClient client = HttpClients.createDefault();

        CloseableHttpResponse response =
            client.execute(new HttpGet("http://www.example.com/"));

With


import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.cache.CachingHttpClients;
...
        CloseableHttpClient client = CachingHttpClients.createMemoryBound();

        CloseableHttpResponse response =
            client.execute(new HttpGet("http://www.example.com/"));

Python workarounds

  • CacheControl(..., heuristic=...)

Java configuration

  • setHeuristicCachingEnabled
  • setSharedCache

Cache-Control in Jersey


    private static final CacheControl SEMISTATIC_CONFIG =
        CacheControl.valueOf("max-age=30, stale-while-revalidate=10");
...
        return Response.ok(...)
                .cacheControl(SEMISTATIC_CONFIG)
                .build();

Conditional requests in Jersey


        EntityTag tag = new EntityTag(..., true);

        ResponseBuilder rb = request.evaluatePreconditions(tag);

        if (rb != null) {
            return rb.build();
        } else {
            ...
            return Response.ok(...).tag(tag)
                       .type(MediaType.APPLICATION_XML_TYPE).build();
        }

Highest-performance implementation?

Better performance

  • Message bus
  • Push
  • Monoliths

Benefits

  • Without complicating a RESTful architecture
  • Without writing any code

Scaling

  • Scales up - low overhead
  • Scales out - farm of caching proxies

Scales down

  • No extra services in development
  • Ignore it and nothing breaks

Blog posts

Recommended reading

HTTPbis

  • RFC 7232 - HTTP/1.1: Conditional Requests
  • RFC 7234 - HTTP/1.1: Caching - browser and intermediary caches

Caching Tutorial for Web Authors and Webmasters

"Although technical in nature, it attempts to make the concepts involved understandable and applicable in real-world situations"

Representational state transfer

"Well-managed caching partially or completely eliminates some client–server interactions, further improving scalability and performance."

In summary

  • Think resources
  • Describe their lifetime
  • Use a caching client

Extras

Namespace hacks

  • /s/ -> Cache-Control: max-age=31536000
  • ?cache-buster=random -> Cache-Control: max-age=0

HTTP/2

HTTP methods, status codes and semantics will be the same, and it should be possible to use the same APIs as HTTP/1.x