--- layout: post title: "Cloudinary: Improving Caching at Scale" meta: "Cache in the cloud: fixing caching at Cloudinary" permalink: /case-studies/cloudinary/ next-case-study-title: "Preparing Raspberry Pi for their upcoming changes" next-case-study-url: /case-studies/raspberry-pi-code-club-workshop/ hide-hire-me-link: true case-study: true lux: "Case Study" --- Right away, there are two important bits of information about this case study: 1. **I am a Cloudinary [_Media Developer Expert_](https://cloudinary.com/blog/announcing_cloudinary_s_media_developer_experts_program).** I am somewhat affiliated with (and love) Cloudinary, but that doesn’t impact the content of this case study. 2. **This was an unpaid project that I conducted as part of fixing issues that I discovered while working on a paid client engagement.** In exchange for notifying Cloudinary about the problem, they said I could write it up. Some time ago, I was helping a firm in Germany get a handle on their Core Web Vitals. It was a fairly routine engagement: they told me what they needed help with, I got to work, we deployed a series of fixes to resounding success:
The majority of my client’s issues were on desktop, so that became the key focus of the engagement.
As part of my investigations, I noticed some oddities with images served from [Cloudinary](https://cloudinary.com/). Cloudinary is an amazing tool for automagically optimising and formatting images on demand without having to reprocess your entire digital asset library—this is almost always a performance win. However, issues in their caching headers were causing unexpected results… ## Reporting the Issue Being a _Media Developer Expert_, I have a good line in to Cloudinary, so I dropped [Tamas Piros](https://x.com/tpiros) a message on Twitter and he put me in touch with the relevant engineers at the company. What follows is a loose transcript of that email thread: ### Initial Report The caching headers set by Cloudinary were a little off: one thing was a definite error, another was a minor contradiction, and another was an opportunistic upgrade. I fired over an email to the following effect: Hi all, I know you’ve given this a lot of thought already, so please don’t think I’m trying to correct you. I’m just going to describe the problem as I see it and let you correct me afterwards if that’s okay…? I’ve run into some caching headaches with a recent client, and it is most likely down to conflicting cache-related headers on image responses. Let’s take an image at random: `https://res.cloudinary.com/[client]/image/upload/[path]/[params]` This carries the following response headers (I’ve marked the cache-related ones): ```bash $ curl -I https://res.cloudinary.com/[client]/image/upload/[path]/[params] HTTP/2 200 content-type: image/jpeg » etag: "e84b0695cd46714fdf658419b4faeb0d" » last-modified: Sat, 02 Jul 2022 02:34:08 GMT date: Tue, 01 Aug 2023 12:19:06 GMT vary: Accept,User-Agent strict-transport-security: max-age=604800 » cache-control: private, no-transform, immutable, max-age=2592000 server-timing: cld-fastly;dur=2;cpu=1;start=2023-08-01T12:19:06.143Z;desc=hit,rtt;dur=31 server: Cloudinary timing-allow-origin: * access-control-allow-origin: * accept-ranges: bytes x-content-type-options: nosniff access-control-expose-headers: Content-Length,ETag,Server-Timing,Vary,X-Content-Type-Options content-length: 11666 ``` The problems I’ve run into are: 1. **`ETag` and `Last-Modified` collide.** A minor issue with minimal side-effects. 2. **`immutable` should not be used here.** This is much more severe and is causing issues for customers. ### `ETag` and `Last-Modified` `ETag` and `Last-Modified` both do the same job—revalidate. However, we don’t need both and would prefer `ETag` over `Last-Modified`. `ETag` only updates if the file _actually_ changes, whereas `Last-Modified` will change any time some metadata changes—`Last-Modified` is a proxy for actual change. This means `Last-Modified` can cause unnecessary downloads due to false positives. #### Recommendation Given that we’re already outputting `ETag` headers, drop `Last-Modified`. **The benefit to customers** is that they don’t incur re-transferred data, thus reducing their costs. **The benefit to end users** is more cache hits and, therefore, improved performance. **The benefit to Cloudinary** is reduced processing, but the drawback is reduced billing (if billing is based on data transfer). ### `immutable` The second thing is much more problematic: we’re using [`immutable`](/2019/03/cache-control-for-civilians/#immutable) in a situation where we shouldn’t use `immutable` at all. `immutable` is like a contract with the browser that says This content will never change and therefore never needs revalidating, even if a user refreshes, etc. You can only safely use `immutable` on files like `app.ca51e8.js`, whose filename is also immutable. This behaviour compounds with the first problem in that we’ve got colliding revalidation headers, and a directive that tells the browser not to revalidate anyway. This renders the revalidation headers ineffective as the browser, as a result of `immutable`, should never emit `If-None-Match` or `If-Modified-Since` request headers. The impact of all of this is that we will very aggressively cache (`immutable`) these responses with no way to actually cache-bust them on the client ([`private`](/2019/03/cache-control-for-civilians/#public-and-private)) for a month (`2592000`). #### Recommendation Remove `immutable`. **The benefit to customers** is that assets are not aggressively cached in unreachable environments. **The benefit to end users** is they are far more likely to see correct and up-to-date content. **The benefit to Cloudinary** is that you’ll now serve _more_ data, which means more earnings if you bill based on throughput. ### `stale-while-revalidate` This isn’t a current issue, rather something I think we as an image provider could make great use of… Given that images (though obviously very important) won’t ever break a site if they’re wrong or out of date (in the way that a wrong JavaScript file might), we’re in the luxurious position to also make use of [`stale-while-revalidate`](/2019/03/cache-control-for-civilians/#stale-while-revalidate)! We can tell the browser to cache and reuse the file for [however long we have already decided](https://cloudinary.com/glossary/caching-images#:~:text=Caching%20images%20with%20Cloudinary) ([`max-age`](/2019/03/cache-control-for-civilians/#max-age)), but also offer a grace period where the browser is permitted to reuse a stale image for a short time while it fetches the new one in the background. Therefore, I propose a new `Cache-Control` header of: ``` Cache-Control: private, no-transform, max-age=2592000, stale-while-revalidate=600 ``` #### Recommendation * **Keep `private` and [`no-transform`](/2019/03/cache-control-for-civilians/#no-transform)**—they’re sensible for sure! * Although `no-transform` is technically redundant for us as we’re running over HTTPS. Leaving it in will have no adverse effects. * **Drop `immutable`** as the assets themselves are not immutable. * **Keep the `max-age`** you’ve already defined. * **Add `stale-while-revalidate`** to offer a 10-minute grace period so that users aren’t left looking at nothing at all. * **Drop `Last-Modified`** in favour of `ETag` as before. ## Reception Feedback was well received. The inclusion of `immutable` was indeed a mistake, so we got that removed with a matter of urgency: > The `immutable` point is interesting – and in fact is indeed a bug; this > should only be sent for a versioned URL, not the unversioned one as in your > example. I have created a task to resolve this – thanks for pointing it out! The `Last-Modified` and `ETag` situation had a few more moving parts, but was an overall happy ending: > With regard to the use of both `Last-Modifiied` and `ETag` – in fact with our > platform this should be correctly handled as we don’t update the modification > date unless something _actually_ changes – simply re-uploading into your Media > Library should not update the date. > > We have customers who still rely on `Last-Modified` for validation in some > embedded systems (part of the reason we handle it as we do). However I am > going to create a QA task internally to ensure there is not a regression > somewhere that has affected that. This is fair! Great context. And, as it turns out clients that understand both `ETag` and `Last-Modified` will prefer `ETag` anyway: we can safely leave both and satisfy all of our needs—no false positives while still supporting outlier customers. > I like the idea of `stale-while-revalidate` – this is something I hadn’t > considered and is nice and safe for legacy clients that wouldn’t support it so > including it should be nice and safe – again, thanks for pointing this out! We didn’t get `stale-while-revalidate` out yet, but I still think it would be a neat addition. Although Cloudinary didn’t hire me directly, I love these kind of collaborative efforts to make far reaching improvements. At Cloudinary’s scale, that’s a lot of people! - - - {% include promo-next.html %}