Using Cache-Control headers in Laravel for HTTP caching with Cloudflare
We recently helped our customer Holocafé prepare its Laravel application for a TV appearance on the German edition of Dragons’ Den (Höhle der Löwen) and the corresponding peak in traffic. The primary change was to utilize Cloudflare as an HTTP reverse proxy to cache the main page of the site and a few other mostly static pages that users were most likely to click on.
The expectation was that 80-90% of the curious users could be served with pages from the HTTP cache.
PHP applications can cause quite a bit of strain on your server resources, which is especially true when the main pages of your site contain static or slowly changing content that is the same for all users, and the same HTML output is generated over and over again.
A reverse proxy is capable of caching the HTML output for a certain duration and serving users the identical HTML repeatedly from its cache, without any impact on your servers.
This prevents a significant amount of PHP processing from occurring, and it also prevents the clogging of the PHP-FPM maximum number of children that are required to serve more significant dynamic requests.
What is the Cache-Control header, and how does it work?
Since version 1.1 (1999), the HTTP protocol defines a header ‘Cache-Control’ that allows an HTTP response to signal to clients how they can cache it.
In its simplest form, you declare the response to be public, and a maximum age in seconds that clients are allowed to cache the response. In our case, we wanted to cache pages in Cloudflare for one hour, which this HTTP response header achieves:
Cache-Control: public, max-age=3600
Cloudflare or other HTTP reverse proxies, such as Varnish, will receive this response and cache the HTML on their servers.
For this to work, the responses have to be truly public. There is not a single output component that is allowed to be dynamic based on the user’s request: This means no Cookies, Language based on Header, Session, or other dynamic output based on time or other external sources.
Introducing a Cache-Control Middleware
In Laravel, you can add a middleware to add the Cache-Control header for selected controllers. For the existing Holocafe application we were unable to add this via middleware groups due to refactoring time constraints, but I would recommend using middleware groups to apply this middleware to only those public, deterministic routes where the cache-control header should be set.
Session Middleware incompatible with Cache-Control
For security reasons, it is plausible to assume that the page is no longer considered “public” once the session is initiated. But this Laravel application had the session middleware active on all routes and pages. To work around this, we have to disable the StartSession middleware for the cacheable public pages. With middleware groups, this would be simple, just do not apply the middleware to the public routes.
Without middleware groups it required a few hacks:
- Introduce a CacheableStartSession middleware that extends the conventional StartSession middleware.
- Patch laravel/jetstream to avoid ShareInertiaData middleware to run when no session is initiated.
- Extend Inertia Middleware to skip running when the session is not started.
If you are running into this problem yourself, see our patch to modify these packages. We blogged about using composer patches in a different scenario before.
Inertia incompatible with Cache-Control
The Holocafe Laravel application uses Inertia for the frontend. This implies that the frontend JavaScript code hijacks links and does not perform regular page loads when clicking a link. Instead, it fetches the page via AJAX and replaces only parts of the layout. This is a common pattern that makes websites and page traversal feel faster. However, due to the inability of Cache-Control responses to distinguish between Inertia and non-Inertia requests, the caching mechanism is compromised.
The solution was to use regular, non-Inertia powered links for the traversal between cached public pages.
Application features incompatible with Cache-Control
In the end, there were some features of the Holocafe website that were incompatible with Cache-Control as well. We introduced a feature flag for “cache control enabled” that automatically disabled these features for the short time frame that we needed caching.
Cloudflare requires explicit Cache Rules to actually cache text/html responses
When we rolled all this out we struggled for a while, Cloudflare was not caching the responses even if the Cache-Control header was correctly set. The reason is that, by default, Cloudflare solely caches assets and not HTML pages.
A Cache Rule has to be created that makes all requests with the appropriate headers eligible for caching.
Result: 95% of requests cached
Cloudflare cached 95% of all requests to the Holocafe website during the TV appearance and the 30 minutes of corresponding traffic spike. This includes assets such as images, JavaScript and CSS.
During that period, the 9000 unique visitors only caused approximately 13000 requests hitting the Laravel application, with a maximum of 2000 requests per minute. An amount the PHP application server could handle easily.
In fact, looking at Tideways there was no visible change in performance due to the request spike, everything sailed along smoothly.
In conclusion, the caching on the CDN / reverse proxy was a simple enough investment compared to making the PHP application handle more concurrent requests. Considering there were only a few days to prepare for the TV appearance, this was a good decision, as changing the code of an application not designed from the beginning for high traffic is usually very complicated.
An AI would never be able to dive this deep into technical topics surrounding PHP performance! Follow us on LinkedIn or X and subscribe to our newsletter to get the latest posts.
Let’s dive in and find out what is causing performance bottlenecks! Without Tideways, you’re likely to fish in murky waters attempting to figure it out. Try our free trial today and be enlightened.