<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: caching</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/caching.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-01-28T22:10:08+00:00</updated><author><name>Simon Willison</name></author><entry><title>Adding dynamic features to an aggressively cached website</title><link href="https://simonwillison.net/2026/Jan/28/dynamic-features-static-site/#atom-tag" rel="alternate"/><published>2026-01-28T22:10:08+00:00</published><updated>2026-01-28T22:10:08+00:00</updated><id>https://simonwillison.net/2026/Jan/28/dynamic-features-static-site/#atom-tag</id><summary type="html">
    &lt;p&gt;My blog uses aggressive caching: it sits behind Cloudflare with a 15 minute cache header, which guarantees it can survive even the largest traffic spike to any given page. I've recently added a couple of dynamic features that work in spite of that full-page caching. Here's how those work.&lt;/p&gt;
&lt;h4 id="edit-links-that-are-visible-only-to-me"&gt;Edit links that are visible only to me&lt;/h4&gt;
&lt;p&gt;This is a Django site and I manage it through the Django admin.&lt;/p&gt;
&lt;p&gt;I have &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/blog/models.py#L254-L449"&gt;four types of content&lt;/a&gt; - entries, link posts (aka blogmarks), quotations and notes. Each of those has a different model and hence a different Django admin area.&lt;/p&gt;
&lt;p&gt;I wanted an "edit" link on the public pages that was only visible to me.&lt;/p&gt;
&lt;p&gt;The button looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/edit-link.jpg" alt="Entry footer - it says Posted 27th January 2026 at 9:44 p.m. followed by a square Edit button with an icon." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I solved conditional display of this button with &lt;code&gt;localStorage&lt;/code&gt;. I have a &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/templates/base.html#L89-L105"&gt;tiny bit of JavaScript&lt;/a&gt; which checks to see if the &lt;code&gt;localStorage&lt;/code&gt; key &lt;code&gt;ADMIN&lt;/code&gt; is set and, if it is, displays an edit link based on a data attribute:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addEventListener&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'DOMContentLoaded'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;localStorage&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getItem&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'ADMIN'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelectorAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.edit-page-link'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;el&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getAttribute&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'data-admin-url'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;a&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;createElement&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'a'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;className&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'edit-link'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerHTML&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'&amp;lt;svg&amp;gt;...&amp;lt;/svg&amp;gt; Edit'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;appendChild&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;display&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'block'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you want to see my edit links you can run this snippet of JavaScript:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;localStorage&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setItem&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'ADMIN'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'1'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My Django admin dashboard has &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/templates/admin/index.html#L18-L39"&gt;a custom checkbox&lt;/a&gt; I can click to turn this option on and off in my own browser:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/edit-toggle.jpg" alt="Screenshot of a Tools settings panel with a teal header reading &amp;quot;Tools&amp;quot; followed by three linked options: &amp;quot;Bulk Tag Tool - Add tags to multiple items at once&amp;quot;, &amp;quot;Merge Tags - Merge multiple tags into one&amp;quot;, &amp;quot;SQL Dashboard - Run SQL queries against the database&amp;quot;, and a checked checkbox labeled &amp;quot;Show &amp;quot;Edit&amp;quot; links on public pages&amp;quot;" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="random-navigation-within-a-tag"&gt;Random navigation within a tag&lt;/h4&gt;
&lt;p&gt;Those admin edit links are a very simple pattern. A more interesting one is a feature I added recently for navigating randomly within a tag.&lt;/p&gt;
&lt;p&gt;Here's an animated GIF showing those random tag navigations in action (&lt;a href="https://simonwillison.net/tag/ai-ethics/"&gt;try it here&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/random-by-tag.gif" alt="Animated demo. Starts on the ai-ethics tag page where a new Random button sits next to the feed icon. Clicking that button jumps to a post with that tag and moves the button into the site header - clicking it multiple times jumps to more random items." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;On any of my blog's tag pages you can click the "Random" button to bounce to a random post with that tag. That random button then persists in the header of the page and you can click it to continue bouncing to random items in that same tag.&lt;/p&gt;
&lt;p&gt;A post can have multiple tags, so there needs to be a little bit of persistent magic to remember which tag you are navigating and display the relevant button in the header.&lt;/p&gt;
&lt;p&gt;Once again, this uses &lt;code&gt;localStorage&lt;/code&gt;. Any click to a random button records both the tag and the current timestamp to the &lt;code&gt;random_tag&lt;/code&gt; key in &lt;code&gt;localStorage&lt;/code&gt; before redirecting the user to the &lt;code&gt;/random/name-of-tag/&lt;/code&gt; page, which selects a random post and redirects them there.&lt;/p&gt;
&lt;p&gt;Any time a new page loads, JavaScript checks if that &lt;code&gt;random_tag&lt;/code&gt; key has a value that was recorded within the past 5 seconds. If so, that random button is appended to the header.&lt;/p&gt;
&lt;p&gt;This means that, provided the page loads within 5 seconds of the user clicking the button, the random tag navigation will persist on the page.&lt;/p&gt;
&lt;p&gt;You can &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/templates/base.html#L106-L147"&gt;see the code for that here&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="and-the-prompts"&gt;And the prompts&lt;/h4&gt;
&lt;p&gt;I built the random tag feature entirely using Claude Code for web, prompted from my iPhone. I started with the &lt;code&gt;/random/TAG/&lt;/code&gt; endpoint (&lt;a href="https://gistpreview.github.io/?2e7de58a779271aa5eb6f4abcd412d72/index.html"&gt;full transcript&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build /random/TAG/ - a page which picks a random post (could be an entry or blogmark or note or quote) that has that tag and sends a 302 redirect to it, marked as no-cache so Cloudflare does not cache it&lt;/p&gt;
&lt;p&gt;Use a union to build a list of every content type (a string representing the table out of the four types) and primary key for every item tagged with that tag, then order by random and return the first one&lt;/p&gt;
&lt;p&gt;Then inflate the type and ID into an object and load it and redirect to the URL&lt;/p&gt;
&lt;p&gt;Include tests - it should work by setting up a tag with one of each of the content types and then running in a loop calling that endpoint until it has either returned one of each of the four types or it hits 1000 loops at which point fail with an error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I do not like that solution, some of my tags have thousands of items&lt;/p&gt;
&lt;p&gt;Can we do something clever with a CTE?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/blog/views.py#L737-L762"&gt;something clever with a CTE&lt;/a&gt; solution we ended up with.&lt;/p&gt;
&lt;p&gt;For the "Random post" button (&lt;a href="https://gistpreview.github.io/?d2d3abe380080ceb9e7fb854fa197bff/index.html"&gt;transcript&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Look at most recent commit, then modify the /tags/xxx/ page to have a "Random post" button which looks good and links to the /random/xxx/ page&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Put it before not after the feed icon. It should only display if a tag has more than 5 posts&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And finally, the &lt;code&gt;localStorage&lt;/code&gt; implementation that persists a random tag button in the header (&lt;a href="https://gistpreview.github.io/?8405b84f8e53738c8d4377b2e41dcdef/page-001.html"&gt;transcript&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Review the last two commits. Make it so clicking the Random button on a tag page sets a localStorage value for random_tag with that tag and a timestamp. On any other page view that uses the base item template add JS that checks for that localStorage value and makes sure the timestamp is within 5 seconds. If it is within 5 seconds it adds a "Random name-of-tag" button to the little top navigation bar, styled like the original Random button, which bumps the localStorage timestamp and then sends the user to /random/name-of-tag/ when they click it. In this way clicking "Random" on a tag page will send the user into an experience where they can keep clicking to keep surfing randomly in that topic.&lt;/p&gt;
&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="caching"/><category term="django"/><category term="javascript"/><category term="localstorage"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>tidwall/pogocache</title><link href="https://simonwillison.net/2025/Jul/21/pogocache/#atom-tag" rel="alternate"/><published>2025-07-21T23:58:53+00:00</published><updated>2025-07-21T23:58:53+00:00</updated><id>https://simonwillison.net/2025/Jul/21/pogocache/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tidwall/pogocache"&gt;tidwall/pogocache&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New project from Josh Baker, author of the excellent &lt;code&gt;tg&lt;/code&gt; C geospatial libarry (&lt;a href="https://simonwillison.net/2023/Sep/23/tg-polygon-indexing/"&gt;covered previously&lt;/a&gt;) and various other &lt;a href="https://github.com/tidwall"&gt;interesting projects&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pogocache is fast caching software built from scratch with a focus on low latency and cpu efficency.&lt;/p&gt;
&lt;p&gt;Faster: Pogocache is faster than Memcache, Valkey, Redis, Dragonfly, and Garnet. It has the lowest latency per request, providing the quickest response times. It's optimized to scale from one to many cores, giving you the best single-threaded and multithreaded performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Faster than Memcache and Redis is a big claim! The README includes a &lt;a href="https://github.com/tidwall/pogocache/blob/main/README.md#design-details"&gt;design details&lt;/a&gt; section that explains how the system achieves that performance, using a sharded hashmap inspired by Josh's &lt;a href="https://github.com/tidwall/shardmap"&gt;shardmap&lt;/a&gt; project and clever application of threads.&lt;/p&gt;
&lt;p&gt;Performance aside, the most interesting thing about Pogocache is the server interface it provides: it emulates the APIs for Redis and Memcached, provides a simple HTTP API &lt;em&gt;and&lt;/em&gt; lets you talk to it over the PostgreSQL wire protocol as well!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql -h localhost -p 9401
=&amp;gt; SET first Tom;
=&amp;gt; SET last Anderson;
=&amp;gt; SET age 37;

$ curl http://localhost:9401/last
Anderson
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=44638076"&gt;Show HN&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/redis"&gt;redis&lt;/a&gt;&lt;/p&gt;



</summary><category term="c"/><category term="caching"/><category term="http"/><category term="memcached"/><category term="postgresql"/><category term="redis"/></entry><entry><title>Double-keyed Caching: How Browser Cache Partitioning Changed the Web</title><link href="https://simonwillison.net/2025/Jan/9/browser-cache-partitioning/#atom-tag" rel="alternate"/><published>2025-01-09T19:00:56+00:00</published><updated>2025-01-09T19:00:56+00:00</updated><id>https://simonwillison.net/2025/Jan/9/browser-cache-partitioning/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://addyosmani.com/blog/double-keyed-caching/"&gt;Double-keyed Caching: How Browser Cache Partitioning Changed the Web&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Addy Osmani provides a clear explanation of how &lt;a href="https://developer.chrome.com/blog/http-cache-partitioning"&gt;browser cache partitioning&lt;/a&gt; has changed the landscape of web optimization tricks.&lt;/p&gt;
&lt;p&gt;Prior to 2020, linking to resources on a shared CDN could provide a performance boost as the user's browser might have already cached that asset from visiting a previous site.&lt;/p&gt;
&lt;p&gt;This opened up privacy attacks, where a malicious site could use the presence of cached assets (based on how long they take to load) to reveal details of sites the user had previously visited.&lt;/p&gt;
&lt;p&gt;Browsers now maintain a separate cache-per-origin. This has had less of an impact than I expected: Chrome's numbers show just a 3.6% increase in overall cache miss rate and 4% increase in bytes loaded from the network.&lt;/p&gt;
&lt;p&gt;The most interesting implication here relates to domain strategy: hosting different aspects of a service on different subdomains now incurs additional cache-related performance costs compared to keeping everything under the same domain.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=42630192"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-performance"&gt;web-performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/addy-osmani"&gt;addy-osmani&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="caching"/><category term="performance"/><category term="web-performance"/><category term="addy-osmani"/></entry><entry><title>Weeknotes: Page caching and custom templates for Datasette Cloud</title><link href="https://simonwillison.net/2024/Jan/7/page-caching-and-custom-templates-for-datasette-cloud/#atom-tag" rel="alternate"/><published>2024-01-07T20:45:11+00:00</published><updated>2024-01-07T20:45:11+00:00</updated><id>https://simonwillison.net/2024/Jan/7/page-caching-and-custom-templates-for-datasette-cloud/#atom-tag</id><summary type="html">
    &lt;p&gt;My main development focus this week has been adding public page caching to &lt;a href="https://www.datasette.cloud/"&gt;Datasette Cloud&lt;/a&gt;, and exploring what custom template support might look like for that service.&lt;/p&gt;
&lt;p&gt;Datasette Cloud primarily provides private "spaces" for teams to collaborate on data. A team can invite additional members, upload CSV files, &lt;a href="https://www.datasette.cloud/docs/api/"&gt;use the API to ingest data&lt;/a&gt;, &lt;a href="https://simonwillison.net/2023/Dec/1/datasette-enrichments/"&gt;run enrichments&lt;/a&gt;, share &lt;a href="https://www.datasette.cloud/blog/2023/datasette-comments/"&gt;private comments&lt;/a&gt; and browse and query the data together.&lt;/p&gt;
&lt;p&gt;The overall goal is to help teams find stories in their data.&lt;/p&gt;
&lt;p&gt;Originally I planned Datasette Cloud as an exclusively private collaboration space, but with hindsight this was a mistake. Datasette has been a tool for publishing data right &lt;a href="https://simonwillison.net/2017/Nov/13/datasette/"&gt;from the start&lt;/a&gt;, and Datasette Cloud users quickly started asking for ways to share their data with the world.&lt;/p&gt;
&lt;p&gt;I started with a plugin for this, &lt;a href="https://github.com/simonw/datasette-public"&gt;datasette-public&lt;/a&gt;, allowing tables to be selectively made visible to unauthenticated users.&lt;/p&gt;
&lt;p&gt;This raised a couple of challenges though. First, I worry about sudden spikes of traffic. Each Datasette Cloud user gets their own dedicated &lt;a href="https://fly.io/"&gt;Fly container&lt;/a&gt; to ensure performance issues are isolated and don't affect other users, but I still don't like the idea of a big public traffic spike taking down a user's site.&lt;/p&gt;
&lt;p&gt;Secondly, some users expressed interest in customizing the display of their public Datasette instance. The open source Datasette application has &lt;a href="https://docs.datasette.io/en/stable/custom_templates.html"&gt;extensive support for this&lt;/a&gt;, but allowing users to run arbitrary HTML and JavaScript on a hosted service is a major risk for XSS holes.&lt;/p&gt;
&lt;p&gt;This week I've been exploring a way to address both of these issues.&lt;/p&gt;
&lt;h4&gt;Full page caching for unauthorized users&lt;/h4&gt;
&lt;p&gt;I've used this trick multiple times through my career - at Lanyrd, at Eventbrite and even for my own personal blog. If a user is signed out, serve them pages through a simple full-page cache - something like Varnish. Set a short TTL on that cache - maybe as short as 15s - such that cached content doesn't have time to go stale.&lt;/p&gt;
&lt;p&gt;Good caches include support for dog-pile prevention, also known as request coalescing. If 10 requests come in for the same page at exactly the same moment, the cache bundles them together and makes just a single request to the backend, then serves the result to all 10 waiting clients.&lt;/p&gt;
&lt;p&gt;How to implement this for Datasette Cloud? My current plan is to use a separate domain - &lt;code&gt;.datasette.site&lt;/code&gt; - for the publicly visible pages of each site. So &lt;code&gt;simon.datasette.cloud&lt;/code&gt; (my personal Datasette Cloud space) would have &lt;code&gt;simon.datasette.site&lt;/code&gt; as its public domain.&lt;/p&gt;
&lt;p&gt;I got this working as a proof-of-concept this week. I actually got it working twice: I figured out how to run a dedicated Varnish instance on Fly, and then I realized that Cloudflare also now &lt;a href="https://blog.cloudflare.com/wildcard-proxy-for-everyone/"&gt;offer wildcard DNS support&lt;/a&gt; so I tried that out too.&lt;/p&gt;
&lt;p&gt;I have both mechanisms up and running at the moment, on two separate domains. I'll likely go with the Cloudflare option to reduce the number of moving parts I'm responsible for myself, but having both means I can compare them to see which one is likely to work best.&lt;/p&gt;
&lt;h4&gt;Custom templates based on host&lt;/h4&gt;
&lt;p&gt;The other reason I decided to explore &lt;code&gt;*.datasette.site&lt;/code&gt; was the security issue I mentioned earlier.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://owasp.org/www-community/attacks/xss/"&gt;XSS attacks&lt;/a&gt;, where malicious JavaScript executes on a trusted domain, are a major security risk.&lt;/p&gt;
&lt;p&gt;I plan to explore additional layers of protection against these such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP"&gt;CSP headers&lt;/a&gt;, but my general rule is to NEVER allow even a chance of untrusted JavaScript executing on a domain where authenticated users are able to perform privileged actions.&lt;/p&gt;
&lt;p&gt;My current plan is to have &lt;code&gt;*.datasette.site&lt;/code&gt; work as an entirely cookie-free domain. Any functionality that requires authentication will be handled by the privileged &lt;code&gt;*.datasette.cloud&lt;/code&gt; domain instead.&lt;/p&gt;
&lt;p&gt;This means I can allow users to provide their own custom templates for their public Datasette instance, without worrying that any mistakes in those templates could lead to a security breach elsewhere within the service.&lt;/p&gt;
&lt;p&gt;There was just one catch: this meant I needed Datasette to be able to use different templates depending on host that the content was being served on.&lt;/p&gt;
&lt;p&gt;After wasting a bunch of time trying to get this to work through monkey-patching, I realized the solution was to add a new plugin hook. &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#jinja2-environment-from-request-datasette-request-env"&gt;jinja2_environment_from_request(datasette, request, env)&lt;/a&gt; is now implemented on &lt;code&gt;main&lt;/code&gt; and should be out in a new alpha release pretty soon. The documentation for that hook includes an example that hints at how I'm using it for Datasette Cloud.&lt;/p&gt;
&lt;h4&gt;Fun further applications of this pattern&lt;/h4&gt;
&lt;p&gt;I'm wary of adding features to Datasette that only serve Datasette Cloud. In this case, I realized that the new plugin hook opens up some interesting possibilities for other users of Datasette.&lt;/p&gt;
&lt;p&gt;I run a bunch of projects on top of Datasette myself - &lt;a href="https://til.simonwillison.net/"&gt;til.simonwillison.net&lt;/a&gt; and &lt;a href="https://www.niche-museums.com/"&gt;www.niche-museums.com&lt;/a&gt; are two examples of my sites that are actually templated Datasette instances.&lt;/p&gt;
&lt;p&gt;Currently, those sites are hosted separately - which means I'm paying to run Datasette multiple times.&lt;/p&gt;
&lt;p&gt;With the ability to serve different templates based on host, I've realized I could instead serve a single Datasette instance for multiple sites, each with their own custom templates.&lt;/p&gt;
&lt;p&gt;Taking advantage of CNAMEs - or even wildcard DNS - means I could run a whole family of weird personal projects on a single instance without any incremental cost for each new project!&lt;/p&gt;
&lt;h4&gt;Releases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-upgrade/releases/tag/0.1a0"&gt;datasette-upgrade 0.1a0&lt;/a&gt;&lt;/strong&gt; - 2024-01-06&lt;br /&gt;Upgrade Datasette instance configuration to handle new features&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TILs&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/github-actions/daily-planner"&gt;GitHub Actions, Issues and Pages to build a daily planner&lt;/a&gt; - 2024-01-02&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/varnish"&gt;varnish&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xss"&gt;xss&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="caching"/><category term="security"/><category term="varnish"/><category term="xss"/><category term="datasette"/><category term="cloudflare"/><category term="weeknotes"/><category term="datasette-cloud"/></entry><entry><title>Cloudflare does not consider vary values in caching decisions</title><link href="https://simonwillison.net/2023/Nov/20/cloudflare-does-not-consider-vary-values-in-caching-decisions/#atom-tag" rel="alternate"/><published>2023-11-20T05:08:52+00:00</published><updated>2023-11-20T05:08:52+00:00</updated><id>https://simonwillison.net/2023/Nov/20/cloudflare-does-not-consider-vary-values-in-caching-decisions/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developers.cloudflare.com/cache/concepts/cache-control/#other"&gt;Cloudflare does not consider vary values in caching decisions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here’s the spot in Cloudflare’s documentation where they hide a crucially important detail:&lt;/p&gt;

&lt;p&gt;“Cloudflare does not consider vary values in caching decisions. Nevertheless, vary values are respected when Vary for images is configured and when the vary header is vary: accept-encoding.”&lt;/p&gt;

&lt;p&gt;This means you can’t deploy an application that uses content negotiation via the Accept header behind the Cloudflare CDN—for example serving JSON or HTML for the same URL depending on the incoming Accept header. If you do, Cloudflare may serve cached JSON to an HTML client or vice-versa.&lt;/p&gt;

&lt;p&gt;There’s an exception for image files, which Cloudflare added support for in September 2021 (for Pro accounts only) in order to support formats such as WebP which may not have full support across all browsers.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="http"/><category term="cloudflare"/></entry><entry><title>New HTTP standards for caching on the modern web</title><link href="https://simonwillison.net/2021/Oct/21/new-http-standards-for-caching-on-the-modern-web/#atom-tag" rel="alternate"/><published>2021-10-21T22:40:50+00:00</published><updated>2021-10-21T22:40:50+00:00</updated><id>https://simonwillison.net/2021/Oct/21/new-http-standards-for-caching-on-the-modern-web/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://httptoolkit.tech/blog/status-targeted-caching-headers/`"&gt;New HTTP standards for caching on the modern web&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cache-Status is a new HTTP header (RFC from August 2021) designed to provide better debugging information about which caches were involved in serving a request—“Cache-Status: Nginx; hit, Cloudflare; fwd=stale; fwd-status=304; collapsed; ttl=300” for example indicates that Nginx served a cache hit, then Cloudflare had a stale cached version so it revalidated from Nginx, got a 304 not modified, collapsed multiple requests (dogpile prevention) and plans to serve the new cached value for the next five minutes. Also described is $Target-Cache-Control: which allows different CDNs to respond to different headers and is already supported by Cloudflare and Akamai (Cloudflare-CDN-Cache-Control: and Akamai-Cache-Control:).

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=28930941"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dogpile"&gt;dogpile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="dogpile"/><category term="http"/><category term="cloudflare"/></entry><entry><title>Last Mile Redis</title><link href="https://simonwillison.net/2021/Jul/17/last-mile-redis/#atom-tag" rel="alternate"/><published>2021-07-17T02:44:55+00:00</published><updated>2021-07-17T02:44:55+00:00</updated><id>https://simonwillison.net/2021/Jul/17/last-mile-redis/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fly.io/blog/last-mile-redis/"&gt;Last Mile Redis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fly.io article about running a local redis cache in each of their geographic regions—“Cache data overlaps a lot less than you assume it will. For the most part, people in Singapore will rely on a different subset of data than people in Amsterdam or São Paulo or New Jersey.” But then they note that Redis has the ability to act as both a replica of a primary AND a writable server at the same time (“replica-read-only no”), which actually makes sense for a cache—it lets you cache local data but send out cluster-wide cache purges if necessary.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=27861510"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/redis"&gt;redis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="redis"/><category term="fly"/></entry><entry><title>nginx proxy-cache-lock</title><link href="https://simonwillison.net/2017/Nov/14/proxy-cache-lock/#atom-tag" rel="alternate"/><published>2017-11-14T21:53:07+00:00</published><updated>2017-11-14T21:53:07+00:00</updated><id>https://simonwillison.net/2017/Nov/14/proxy-cache-lock/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_lock"&gt;nginx proxy-cache-lock&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Crucially important feature hidden away in the nginx documentation: proxy_cache_lock enables request coalescing, or dog-pile protection: it means that if a hundred simultaneous requests all suffer the same cache miss, only one request is made to the backend and the answer is then sent back to all hundred requests at once. I’ve leaned heavily on this feature in Varnish for years—useful to know that nginx has the same capability.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://blog.discordapp.com/how-discord-resizes-150-million-images-every-day-with-go-and-c-c9e98731c65d"&gt;How Discord Resizes 150 Million Images Every Day with Go and C++&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dogpile"&gt;dogpile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nginx"&gt;nginx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/varnish"&gt;varnish&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="dogpile"/><category term="nginx"/><category term="varnish"/></entry><entry><title>Display your events on your own website with Lanyrd Badges</title><link href="https://simonwillison.net/2011/Jan/13/badges/#atom-tag" rel="alternate"/><published>2011-01-13T20:38:00+00:00</published><updated>2011-01-13T20:38:00+00:00</updated><id>https://simonwillison.net/2011/Jan/13/badges/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://lanyrd.com/blog/2011/badges/"&gt;Display your events on your own website with Lanyrd Badges&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
We’ve launched badges for Lanyrd—JavaScript that lets you embed a top bar or a content “splat” showing events you plan to attend, talks you’ve given in the past and other various combinations. I’m quite pleased with the implementation—the badges are configured using classes on a link to your Lanyrd profile, and the badges themselves are served through a combination of Amazon CloudFront for the initial script and a Varnish cache for the badge data itself to keep things nice and snappy.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/badges"&gt;badges&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudfront"&gt;cloudfront&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/varnish"&gt;varnish&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/lanyrd"&gt;lanyrd&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="badges"/><category term="caching"/><category term="cloudfront"/><category term="javascript"/><category term="varnish"/><category term="lanyrd"/><category term="recovered"/></entry><entry><title>Reddit is now running on Cassandra</title><link href="https://simonwillison.net/2010/Mar/13/cassandra/#atom-tag" rel="alternate"/><published>2010-03-13T00:14:27+00:00</published><updated>2010-03-13T00:14:27+00:00</updated><id>https://simonwillison.net/2010/Mar/13/cassandra/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.reddit.com/2010/03/she-who-entangles-men.html"&gt;Reddit is now running on Cassandra&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Migrating their persistent cache over from memcacheDB to Cassandra took one developer just ten days.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cassandra"&gt;cassandra&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcachedb"&gt;memcachedb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/reddit"&gt;reddit&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="cassandra"/><category term="memcachedb"/><category term="reddit"/></entry><entry><title>Cache Machine: Automatic caching for your Django models</title><link href="https://simonwillison.net/2010/Mar/11/cachemachine/#atom-tag" rel="alternate"/><published>2010-03-11T19:35:32+00:00</published><updated>2010-03-11T19:35:32+00:00</updated><id>https://simonwillison.net/2010/Mar/11/cachemachine/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://jbalogh.me/2010/02/09/cache-machine/"&gt;Cache Machine: Automatic caching for your Django models&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is the third new ORM caching layer for Django I’ve seen in the past month! Cache Machine was developed for zamboni, the port of addons.mozilla.org to Django. Caching is enabled using a model mixin class (to hook up some post_delete hooks) and a custom caching manager. Invalidation works by maintaining a “flush list” of dependent cache entries for each object—this is currently stored in memcached and hence has potential race conditions, but a comment in the source code suggests that this could be solved by moving to redis.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cachemachine"&gt;cachemachine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ormcaching"&gt;ormcaching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/redis"&gt;redis&lt;/a&gt;&lt;/p&gt;



</summary><category term="cachemachine"/><category term="caching"/><category term="django"/><category term="memcached"/><category term="mozilla"/><category term="orm"/><category term="ormcaching"/><category term="python"/><category term="redis"/></entry><entry><title>Announcing django-cachebot</title><link href="https://simonwillison.net/2010/Mar/6/david/#atom-tag" rel="alternate"/><published>2010-03-06T12:48:39+00:00</published><updated>2010-03-06T12:48:39+00:00</updated><id>https://simonwillison.net/2010/Mar/6/david/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.davidziegler.net/post/429237463/announcing-django-cachebot"&gt;Announcing django-cachebot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The ORM caching space around Django is heating up. django-cachebot is used in production at mingle.com and takes a more low level approach to cache invalidation than Johnny Cache, enabling you to specifically mark the querysets you wish to cache and providing some advanced options for cache invalidation. Unfortunately it currently relies on a patch to Django core to enable its own manager.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cachebot"&gt;cachebot&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mingle"&gt;mingle&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ormcaching"&gt;ormcaching&lt;/a&gt;&lt;/p&gt;



</summary><category term="cachebot"/><category term="caching"/><category term="django"/><category term="mingle"/><category term="orm"/><category term="ormcaching"/></entry><entry><title>Is johnny-cache for you?</title><link href="https://simonwillison.net/2010/Mar/2/johnny/#atom-tag" rel="alternate"/><published>2010-03-02T11:44:08+00:00</published><updated>2010-03-02T11:44:08+00:00</updated><id>https://simonwillison.net/2010/Mar/2/johnny/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://jmoiron.net/blog/is-johnny-cache-for-you/"&gt;Is johnny-cache for you?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“Using Johnny is really adopting a particular caching strategy. This strategy isn’t always a win; it can impact performance negatively”—but for a high percentage of Django sites there’s a very good chance it will be a net bonus.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/johnnycache"&gt;johnnycache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="django"/><category term="johnnycache"/><category term="performance"/><category term="python"/></entry><entry><title>jmoiron.net: Johnny Cache</title><link href="https://simonwillison.net/2010/Mar/1/jmoironnet/#atom-tag" rel="alternate"/><published>2010-03-01T11:48:54+00:00</published><updated>2010-03-01T11:48:54+00:00</updated><id>https://simonwillison.net/2010/Mar/1/jmoironnet/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://django.jmoiron.net/blog/johnny-cache/"&gt;jmoiron.net: Johnny Cache&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The blog entry announcing Johnny Cache (“a drop-in caching library/framework for Django that will cache all of your querysets forever in a consistent and safe manner”) to the world.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/johnnycache"&gt;johnnycache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/querysets"&gt;querysets&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="django"/><category term="johnnycache"/><category term="python"/><category term="querysets"/></entry><entry><title>Johnny Cache</title><link href="https://simonwillison.net/2010/Feb/28/johnny/#atom-tag" rel="alternate"/><published>2010-02-28T22:55:15+00:00</published><updated>2010-02-28T22:55:15+00:00</updated><id>https://simonwillison.net/2010/Feb/28/johnny/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://packages.python.org/johnny-cache/"&gt;Johnny Cache&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Clever twist on ORM-level caching for Django. Johnny Cache (great name) monkey-patches Django’s QuerySet classes and caches the result of every single SELECT query in memcached with an infinite expiry time. The cache key includes a “generation” ID for each dependent database table, and the generation is changed every single time a table is updated. For apps with infrequent writes, this strategy should work really well—but if a popular table is being updated constantly the cache will be all but useless. Impressively, the system is transaction-aware—cache entries created during a transaction are held in local memory and only pushed to memcached should the transaction complete successfully.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ormcaching"&gt;ormcaching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="databases"/><category term="django"/><category term="memcached"/><category term="orm"/><category term="ormcaching"/><category term="performance"/><category term="python"/></entry><entry><title>Internet Explorer Cookie Internals (FAQ)</title><link href="https://simonwillison.net/2010/Feb/26/cachebusting/#atom-tag" rel="alternate"/><published>2010-02-26T12:25:24+00:00</published><updated>2010-02-26T12:25:24+00:00</updated><id>https://simonwillison.net/2010/Feb/26/cachebusting/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blogs.msdn.com/ieinternals/archive/2009/08/20/WinINET-IE-Cookie-Internals-FAQ.aspx"&gt;Internet Explorer Cookie Internals (FAQ)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Grr... IE 6, 7 and 8 don’t support the max-age cookie argument, forcing you to use an explicit expiry date instead. This appears to affect the cache busting cookie pattern, where you set a cookie to expire in 30 seconds for any user who posts content and use the presence of that cookie to skip caches and/or send their queries to a master instead of slave database. If you have to use expires, users with incorrect system clocks may get inconsistent results. Anyone know of a workaround?


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cachebusting"&gt;cachebusting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/internet-explorer"&gt;internet-explorer&lt;/a&gt;&lt;/p&gt;



</summary><category term="cachebusting"/><category term="caching"/><category term="cookies"/><category term="internet-explorer"/></entry><entry><title>High-end Varnish-tuning</title><link href="https://simonwillison.net/2009/Oct/20/highend/#atom-tag" rel="alternate"/><published>2009-10-20T09:25:42+00:00</published><updated>2009-10-20T09:25:42+00:00</updated><id>https://simonwillison.net/2009/Oct/20/highend/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://kristian.blog.linpro.no/2009/10/19/high-end-varnish-tuning/"&gt;High-end Varnish-tuning&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tuning the Varnish HTTP cache to serve 27K requests/second on a single core 2.2GHz Opteron.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/varnish"&gt;varnish&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="http"/><category term="performance"/><category term="varnish"/></entry><entry><title>Caching in ASP.NET with the SqlCacheDependency Class</title><link href="https://simonwillison.net/2009/Aug/18/caching/#atom-tag" rel="alternate"/><published>2009-08-18T12:15:44+00:00</published><updated>2009-08-18T12:15:44+00:00</updated><id>https://simonwillison.net/2009/Aug/18/caching/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://msdn.microsoft.com/en-us/library/ms178604.aspx"&gt;Caching in ASP.NET with the SqlCacheDependency Class&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Interesting cache invalidation concept: set up dependencies between cache entries and tables or rows in the database, then use triggers (which I presume are automatically created for you) to clear your cache.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aspdotnet"&gt;aspdotnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/invalidation"&gt;invalidation&lt;/a&gt;&lt;/p&gt;



</summary><category term="aspdotnet"/><category term="caching"/><category term="invalidation"/></entry><entry><title>Memcached 1.4.0 released</title><link href="https://simonwillison.net/2009/Jul/17/memcached/#atom-tag" rel="alternate"/><published>2009-07-17T22:26:48+00:00</published><updated>2009-07-17T22:26:48+00:00</updated><id>https://simonwillison.net/2009/Jul/17/memcached/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://dustin.github.com/2009/07/16/memcached-1.4.html"&gt;Memcached 1.4.0 released&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The big new feature is the (optional) binary protocol, which enables other features such as CAS-everywhere and efficient client-side replication. Maintainer Dustin Sallings has also released some useful sounding EC2 instances which automatically assign nearly all of their RAM to memcached on launch and shouldn’t need any further configuration.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ami"&gt;ami&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/binary"&gt;binary&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cas"&gt;cas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dustin-sallings"&gt;dustin-sallings&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ec2"&gt;ec2&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;&lt;/p&gt;



</summary><category term="ami"/><category term="binary"/><category term="caching"/><category term="cas"/><category term="dustin-sallings"/><category term="ec2"/><category term="memcached"/><category term="performance"/><category term="scaling"/></entry><entry><title>Yahoo! proposal to open source "Traffic Server" via the ASF</title><link href="https://simonwillison.net/2009/Jul/7/trafficserver/#atom-tag" rel="alternate"/><published>2009-07-07T12:37:02+00:00</published><updated>2009-07-07T12:37:02+00:00</updated><id>https://simonwillison.net/2009/Jul/7/trafficserver/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://wiki.apache.org/incubator/TrafficServerProposal"&gt;Yahoo! proposal to open source &amp;quot;Traffic Server&amp;quot; via the ASF&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Traffic Server is a “fast, scalable and extensible HTTP/1.1 compliant  caching proxy server” (presumably equivalent to things like Squid and Varnish) originally acquired from Inktomi and developed internally at Yahoo! for the past three years, which has been benchmarked handling 35,000 req/s on a single box. No source code yet but it looks like the release will arrive pretty soon.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apache"&gt;apache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asf"&gt;asf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/proxies"&gt;proxies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/squid"&gt;squid&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/trafficserver"&gt;trafficserver&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/varnish"&gt;varnish&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yahoo"&gt;yahoo&lt;/a&gt;&lt;/p&gt;



</summary><category term="apache"/><category term="asf"/><category term="caching"/><category term="open-source"/><category term="proxies"/><category term="squid"/><category term="trafficserver"/><category term="varnish"/><category term="yahoo"/></entry><entry><title>cache-money</title><link href="https://simonwillison.net/2009/Jun/28/cachemoney/#atom-tag" rel="alternate"/><published>2009-06-28T15:17:30+00:00</published><updated>2009-06-28T15:17:30+00:00</updated><id>https://simonwillison.net/2009/Jun/28/cachemoney/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://github.com/nkallen/cache-money/tree/master"&gt;cache-money&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A “write-through caching library for ActiveRecord”, maintained by Nick Kallen from Twitter. Queries hit memcached first, and caches are automatically kept up-to-date when objects are created, updated and deleted. Only some queries are supported—joins and comparisons won’t hit the cache, for example.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/activerecord"&gt;activerecord&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cachemoney"&gt;cachemoney&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;&lt;/p&gt;



</summary><category term="activerecord"/><category term="cachemoney"/><category term="caching"/><category term="memcached"/><category term="rails"/><category term="twitter"/></entry><entry><title>Twitter, an Evolving Architecture</title><link href="https://simonwillison.net/2009/Jun/28/twitter/#atom-tag" rel="alternate"/><published>2009-06-28T15:09:44+00:00</published><updated>2009-06-28T15:09:44+00:00</updated><id>https://simonwillison.net/2009/Jun/28/twitter/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.infoq.com/news/2009/06/Twitter-Architecture"&gt;Twitter, an Evolving Architecture&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The most detailed write-up of Twitter’s current architecture I’ve seen, explaining the four layers of cache (all memcached) used by the Twitter API.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/architecture"&gt;architecture&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;&lt;/p&gt;



</summary><category term="architecture"/><category term="caching"/><category term="memcached"/><category term="twitter"/></entry><entry><title>Django tip: Caching and two-phased template rendering</title><link href="https://simonwillison.net/2009/May/19/twophased/#atom-tag" rel="alternate"/><published>2009-05-19T01:34:35+00:00</published><updated>2009-05-19T01:34:35+00:00</updated><id>https://simonwillison.net/2009/May/19/twophased/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.holovaty.com/writing/django-two-phased-rendering/"&gt;Django tip: Caching and two-phased template rendering&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat trick for expensive pages which can be mostly cached with the exception of the “logged in as” bit—run them through the template system twice, caching the intermediary generated template.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/adrian-holovaty"&gt;adrian-holovaty&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/template"&gt;template&lt;/a&gt;&lt;/p&gt;



</summary><category term="adrian-holovaty"/><category term="caching"/><category term="django"/><category term="performance"/><category term="python"/><category term="template"/></entry><entry><title>mmalone's django-caching</title><link href="https://simonwillison.net/2009/May/7/mmalones/#atom-tag" rel="alternate"/><published>2009-05-07T07:36:48+00:00</published><updated>2009-05-07T07:36:48+00:00</updated><id>https://simonwillison.net/2009/May/7/mmalones/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://github.com/mmalone/django-caching"&gt;mmalone&amp;#x27;s django-caching&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Mike Malone shares code used by Pownce to add QuerySet level caching to Django. It’s a smart implementation—a CachingQuerySet class inspects the arguments passed to get(), and if they’re just a straight forward exact PK lookup hits memcache for the object before hitting the database. Signals are used to invalidate the cache.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mike-malone"&gt;mike-malone&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pownce"&gt;pownce&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/querysets"&gt;querysets&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/signals"&gt;signals&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="django"/><category term="mike-malone"/><category term="pownce"/><category term="querysets"/><category term="signals"/></entry><entry><title>hash_ring 1.2</title><link href="https://simonwillison.net/2009/May/5/hashring/#atom-tag" rel="alternate"/><published>2009-05-05T13:45:08+00:00</published><updated>2009-05-05T13:45:08+00:00</updated><id>https://simonwillison.net/2009/May/5/hashring/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://pypi.python.org/pypi/hash_ring/1.2"&gt;hash_ring 1.2&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A Python library for consistent hashing with memcached, using MD5 and the same algorithm as libketama. Exposes an interface that is identical to regular memcache making this a drop-in replacement.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/amir-salihefendic"&gt;amir-salihefendic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/consistenthashing"&gt;consistenthashing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hashring"&gt;hashring&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/libketama"&gt;libketama&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/md5"&gt;md5&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="amir-salihefendic"/><category term="caching"/><category term="consistenthashing"/><category term="django"/><category term="hashring"/><category term="libketama"/><category term="md5"/><category term="memcached"/><category term="python"/></entry><entry><title>How search.twitter.com uses Varnish</title><link href="https://simonwillison.net/2009/Mar/2/varnish/#atom-tag" rel="alternate"/><published>2009-03-02T17:08:13+00:00</published><updated>2009-03-02T17:08:13+00:00</updated><id>https://simonwillison.net/2009/Mar/2/varnish/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://projects.linpro.no/pipermail/varnish-dev/2009-February/000968.html"&gt;How search.twitter.com uses Varnish&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Includes examples of the configuration options they use.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/search"&gt;search&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/varnish"&gt;varnish&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="search"/><category term="twitter"/><category term="varnish"/></entry><entry><title>Sharding Counters on Google App Engine</title><link href="https://simonwillison.net/2009/Jan/27/sharding/#atom-tag" rel="alternate"/><published>2009-01-27T20:27:41+00:00</published><updated>2009-01-27T20:27:41+00:00</updated><id>https://simonwillison.net/2009/Jan/27/sharding/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://code.google.com/appengine/articles/sharding_counters.html"&gt;Sharding Counters on Google App Engine&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“While the datastore for App Engine scales to support a huge number of entities it is important to note that you can only expect to update any single entity, or entity-group, about five times a second”. This article explains a technique for sharding writes across multiple counters in detail, including a way to keep a memcache counter updated at the same time for faster reads.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-app-engine"&gt;google-app-engine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcache"&gt;memcache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sharding"&gt;sharding&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="google"/><category term="google-app-engine"/><category term="memcache"/><category term="python"/><category term="scaling"/><category term="sharding"/></entry><entry><title>ETags And Modification Times In Django</title><link href="https://simonwillison.net/2008/Dec/13/etags/#atom-tag" rel="alternate"/><published>2008-12-13T09:49:42+00:00</published><updated>2008-12-13T09:49:42+00:00</updated><id>https://simonwillison.net/2008/Dec/13/etags/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.pointy-stick.com/blog/2008/12/13/etags-and-modification-times-django/"&gt;ETags And Modification Times In Django&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Part of Malcolm’s series of tutorials on implementing advanced HTTP concepts in Django.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/etags"&gt;etags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/malcolm-tredinnick"&gt;malcolm-tredinnick&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="django"/><category term="etags"/><category term="http"/><category term="malcolm-tredinnick"/></entry><entry><title>REST, I just don't get it</title><link href="https://simonwillison.net/2008/Aug/15/katz/#atom-tag" rel="alternate"/><published>2008-08-15T08:20:04+00:00</published><updated>2008-08-15T08:20:04+00:00</updated><id>https://simonwillison.net/2008/Aug/15/katz/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://damienkatz.net/2008/08/rest-i-just-dont-get-it.html"&gt;REST, I just don&amp;#x27;t get it&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Read the comments for some excellent practical reasons to care about REST, including cache management (PUT and DELETE can expire the cache entries for the corresponding GET), the ability to add or move parts of the server API without redeploying client libraries and the idempotency of GET / PUT / DELETE and HEAD (repeated POST operations may have side-effects).


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/damien-katz"&gt;damien-katz&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/delete"&gt;delete&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/get"&gt;get&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/idempotency"&gt;idempotency&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/post"&gt;post&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/put"&gt;put&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rest"&gt;rest&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="damien-katz"/><category term="delete"/><category term="get"/><category term="idempotency"/><category term="post"/><category term="put"/><category term="rest"/></entry><entry><title>Facelift Image Replacement</title><link href="https://simonwillison.net/2008/Aug/5/alternative/#atom-tag" rel="alternate"/><published>2008-08-05T18:36:19+00:00</published><updated>2008-08-05T18:36:19+00:00</updated><id>https://simonwillison.net/2008/Aug/5/alternative/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://facelift.mawhorter.net/"&gt;Facelift Image Replacement&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Like sIFR but with JavaScript and a PHP text rendering component. I question the need for the JavaScript if you’re already generating the images on the server, but the actual generation script is nicely done—it makes smart use of ImageMagick and caches the generated images.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/facelift"&gt;facelift&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/imagemagick"&gt;imagemagick&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/imagereplacement"&gt;imagereplacement&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sifr"&gt;sifr&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="facelift"/><category term="imagemagick"/><category term="imagereplacement"/><category term="javascript"/><category term="php"/><category term="sifr"/></entry></feed>