<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: cloudflare</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/cloudflare.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-22T19:16:30+00:00</updated><author><name>Simon Willison</name></author><entry><title>DNS Lookup</title><link href="https://simonwillison.net/2026/Mar/22/dns/#atom-tag" rel="alternate"/><published>2026-03-22T19:16:30+00:00</published><updated>2026-03-22T19:16:30+00:00</updated><id>https://simonwillison.net/2026/Mar/22/dns/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Tool:&lt;/strong&gt; &lt;a href="https://tools.simonwillison.net/dns"&gt;DNS Lookup&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;TIL that Cloudflare's 1.1.1.1 DNS service (and 1.1.1.2 and 1.1.1.3, which block malware and malware + adult content respectively) has a CORS-enabled JSON API, so I &lt;a href="https://github.com/simonw/tools/pull/258#issue-4116864108"&gt;had Claude Code build me&lt;/a&gt; a UI for running DNS queries against all three of those resolvers.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/dns"&gt;dns&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="dns"/><category term="cors"/><category term="cloudflare"/></entry><entry><title>tldraw issue: Move tests to closed source repo</title><link href="https://simonwillison.net/2026/Feb/25/closed-tests/#atom-tag" rel="alternate"/><published>2026-02-25T21:06:53+00:00</published><updated>2026-02-25T21:06:53+00:00</updated><id>https://simonwillison.net/2026/Feb/25/closed-tests/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tldraw/tldraw/issues/8082"&gt;tldraw issue: Move tests to closed source repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It's become very apparent over the past few months that a comprehensive test suite is enough to build a completely fresh implementation of any open source library from scratch, potentially in a different language.&lt;/p&gt;
&lt;p&gt;This has worrying implications for open source projects with commercial business models. Here's an example of a response: tldraw, the outstanding collaborative drawing library (see &lt;a href="https://simonwillison.net/2023/Nov/16/tldrawdraw-a-ui/"&gt;previous coverage&lt;/a&gt;), are moving their test suite to a private repository - apparently in response to &lt;a href="https://blog.cloudflare.com/vinext/"&gt;Cloudflare's project to port Next.js to use Vite in a week using AI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;They also filed a joke issue, now closed to &lt;a href="https://github.com/tldraw/tldraw/issues/8092"&gt;Translate source code to Traditional Chinese&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The current tldraw codebase is in English, making it easy for external AI coding agents to replicate. It is imperative that we defend our intellectual property.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Worth noting that tldraw aren't technically open source - their &lt;a href="https://github.com/tldraw/tldraw?tab=License-1-ov-file#readme"&gt;custom license&lt;/a&gt; requires a commercial license if you want to use it in "production environments".&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Well this is embarrassing, it turns out the issue I linked to about removing the tests was &lt;a href="https://github.com/tldraw/tldraw/issues/8082#issuecomment-3964650501"&gt;a joke as well&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sorry folks, this issue was more of a joke (am I allowed to do that?) but I'll keep the issue open since there's some discussion here. Writing from mobile&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;moving our tests into another repo would complicate and slow down our development, and speed for us is more important than ever&lt;/li&gt;
&lt;li&gt;more canvas better, I know for sure that our decisions have inspired other products and that's fine and good&lt;/li&gt;
&lt;li&gt;tldraw itself may eventually be a vibe coded alternative to tldraw&lt;/li&gt;
&lt;li&gt;the value is in the ability to produce new and good product decisions for users / customers, however you choose to create the code&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/steveruizok/status/2026581824428753211"&gt;@steveruizok&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="cloudflare"/><category term="ai-ethics"/></entry><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>Using Claude in Chrome to navigate out the Cloudflare dashboard</title><link href="https://simonwillison.net/2025/Dec/22/claude-chrome-cloudflare/#atom-tag" rel="alternate"/><published>2025-12-22T16:10:30+00:00</published><updated>2025-12-22T16:10:30+00:00</updated><id>https://simonwillison.net/2025/Dec/22/claude-chrome-cloudflare/#atom-tag</id><summary type="html">
    &lt;p&gt;I just had my first success using a browser agent - in this case the &lt;a href="https://support.claude.com/en/articles/12012173-getting-started-with-claude-in-chrome"&gt;Claude in Chrome extension&lt;/a&gt; - to solve an actual problem.&lt;/p&gt;
&lt;p&gt;A while ago I set things up so anything served from the &lt;code&gt;https://static.simonwillison.net/static/cors-allow/&lt;/code&gt; directory of my S3 bucket would have open &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; headers. This is useful for hosting files online that can be loaded into web applications hosted on other domains.&lt;/p&gt;
&lt;p&gt;Problem is I couldn't remember how I did it! I initially thought it was an S3 setting, but it turns out S3 lets you set CORS at the bucket-level but not for individual prefixes.&lt;/p&gt;
&lt;p&gt;I then suspected Cloudflare, but I find the Cloudflare dashboard really difficult to navigate.&lt;/p&gt;
&lt;p&gt;So I decided to give Claude in Chrome a go. I installed and enabled the extension (you then have to click the little puzzle icon and click "pin" next to Claude for the icon to appear, I had to ask Claude itself for help figuring that out), signed into Cloudflare, opened the Claude panel and prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;I'm trying to figure out how come all pages under http://static.simonwillison.net/static/cors/ have an open CORS policy, I think I set that up through Cloudflare but I can't figure out where&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Off it went. It took 1m45s to find exactly what I needed.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of Chrome - on the left is the Cloudflare dashboard. On the right a Claude chat panel which shows the final result: Summary  Found it! Your open CORS policy for the /static/cors/ directory on static.simonwillison.net is configured through Cloudflare Transform Rules, specifically a Response Header Transform Rule named static.simonwillis.net/static/cors-allow/*.  Here's exactly what's set up:  Rule Configuration:      Type: HTTP Response Header Transform Rule     Applies to: Any URL matching https://static.simonwillison.net/static/cors-allow/* (wildcard match)     Action: Modifies the response header access-control-allow-origin and sets it to *  This is why all pages under that path have an open CORS policy — the * value for Access-Control-Allow-Origin means any origin can access those resources.  Where to find/modify it in the dashboard:      Go to your domain settings (simonwillison.net)     Navigate to Rules &amp;gt; Rules Overview (in the left sidebar)     Scroll down to Response Header Transform Rules (should show 2 active)     Click on the rule named static.simonwillis.net/static/cors-allow/* to view or edit it  " src="https://static.simonwillison.net/static/2025/claude-chrome-cloudflare.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Claude's conclusion:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Found it! Your open CORS policy for the &lt;code&gt;/static/cors/&lt;/code&gt; directory on &lt;code&gt;static.simonwillison.net&lt;/code&gt; is configured through &lt;strong&gt;Cloudflare Transform Rules&lt;/strong&gt;, specifically a &lt;strong&gt;Response Header Transform Rule&lt;/strong&gt; named &lt;code&gt;static.simonwillis.net/static/cors-allow/*&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There's no "share transcript" option but I used copy and paste and two gnarly Claude Code sessions (&lt;a href="https://gistpreview.github.io/?56adf4212345d9967c22aab1362b847b"&gt;one&lt;/a&gt;, &lt;a href="https://gistpreview.github.io/?1d5f524616bef403cdde4bc92da5b0ba"&gt;two&lt;/a&gt;) to turn it into an HTML transcript which &lt;a href="https://static.simonwillison.net/static/2025/claude-chrome-transcript.html"&gt;you can take a look at here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I remain deeply skeptical of the entire browsing agent category due to my concerns about prompt injection risks—I watched what it was doing here like a &lt;em&gt;hawk&lt;/em&gt;—but I have to admit this was a very positive experience.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browser-agents"&gt;browser-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&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/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-injection"&gt;prompt-injection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-agents"&gt;ai-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="anthropic"/><category term="claude"/><category term="browser-agents"/><category term="cors"/><category term="ai"/><category term="llms"/><category term="generative-ai"/><category term="chrome"/><category term="cloudflare"/><category term="prompt-injection"/><category term="ai-agents"/></entry><entry><title>Quoting Matthew Prince</title><link href="https://simonwillison.net/2025/Nov/19/matthew-prince/#atom-tag" rel="alternate"/><published>2025-11-19T08:02:36+00:00</published><updated>2025-11-19T08:02:36+00:00</updated><id>https://simonwillison.net/2025/Nov/19/matthew-prince/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://blog.cloudflare.com/18-november-2025-outage/"&gt;&lt;p&gt;Cloudflare's network began experiencing significant failures to deliver core network traffic [...] triggered by a change to one of our database systems' permissions which caused the database to output multiple entries into a “feature file” used by our Bot Management system. That feature file, in turn, doubled in size. The larger-than-expected feature file was then propagated to all the machines that make up our network. [...] The software had a limit on the size of the feature file that was below its doubled size. That caused the software to fail. [...]&lt;/p&gt;
&lt;p&gt;This resulted in the following panic which in turn resulted in a 5xx error:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;thread fl2_worker_thread panicked: called Result::unwrap() on an Err value&lt;/code&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://blog.cloudflare.com/18-november-2025-outage/"&gt;Matthew Prince&lt;/a&gt;, Cloudflare outage on November 18, 2025, &lt;a href="https://news.ycombinator.com/item?id=45973709#45974320"&gt;see also this comment&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postmortem"&gt;postmortem&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="rust"/><category term="cloudflare"/><category term="postmortem"/></entry><entry><title>Video: Building a tool to copy-paste share terminal sessions using Claude Code for web</title><link href="https://simonwillison.net/2025/Oct/23/claude-code-for-web-video/#atom-tag" rel="alternate"/><published>2025-10-23T04:14:08+00:00</published><updated>2025-10-23T04:14:08+00:00</updated><id>https://simonwillison.net/2025/Oct/23/claude-code-for-web-video/#atom-tag</id><summary type="html">
    &lt;p&gt;This afternoon I was manually converting a terminal session into a shared HTML file for the umpteenth time when I decided to reduce the friction by building a custom tool for it - and on the spur of the moment I fired up &lt;a href="https://www.descript.com/"&gt;Descript&lt;/a&gt; to record the process. The result is this new &lt;a href="https://www.youtube.com/watch?v=GQvMLLrFPVI"&gt;11 minute YouTube video&lt;/a&gt; showing my workflow for vibe-coding simple tools from start to finish.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid="GQvMLLrFPVI" js-api="js-api"
  title="Using Claude Code for web to build a tool to copy-paste share terminal sessions"
  playlabel="Play: Using Claude Code for web to build a tool to copy-paste share terminal sessions"
&gt; &lt;/lite-youtube&gt;&lt;/p&gt;
&lt;h4 id="the-initial-problem"&gt;The initial problem&lt;/h4&gt;
&lt;p&gt;The problem I wanted to solve involves sharing my Claude Code CLI sessions - and the more general problem of sharing interesting things that happen in my terminal.&lt;/p&gt;
&lt;p&gt;A while back I discovered (using my vibe-coded &lt;a href="https://tools.simonwillison.net/clipboard-viewer"&gt;clipboard inspector&lt;/a&gt;) that copying and pasting from the macOS terminal populates a rich text clipboard format which preserves the colors and general formatting of the terminal output.&lt;/p&gt;
&lt;p&gt;The problem is that format looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{\rtf1\ansi\ansicpg1252\cocoartf2859
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 Monaco;}
{\colortbl;\red255\green255\blue255;\red242\green242\blue242;\red0\green0\blue0;\red204\green98\blue70;
\red0\green0\blue0;\red97\green97\blue97;\red102\green102\blue102;\red255\
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This struck me as the kind of thing an LLM might be able to write code to parse, so I had &lt;a href="https://chatgpt.com/share/680801ad-0804-8006-83fc-c2b209841a9c"&gt;ChatGPT take a crack at it&lt;/a&gt; and then later &lt;a href="https://claude.ai/share/5c12dd0e-713d-4f32-a6c1-d05dee353e4d"&gt;rewrote it from scratch with Claude Sonnet 4.5&lt;/a&gt;. The result was &lt;a href="https://tools.simonwillison.net/rtf-to-html"&gt;this rtf-to-html tool&lt;/a&gt; which lets you paste in rich formatted text and gives you reasonably solid HTML that you can share elsewhere.&lt;/p&gt;
&lt;p&gt;To share that HTML I've started habitually pasting it into a &lt;a href="https://gist.github.com/"&gt;GitHub Gist&lt;/a&gt; and then taking advantage of &lt;code&gt;gitpreview.github.io&lt;/code&gt;, a neat little unofficial tool that accepts &lt;code&gt;?GIST_ID&lt;/code&gt; and displays the gist content as a standalone HTML page... which means you can link to rendered HTML that's stored in a gist.&lt;/p&gt;
&lt;p&gt;So my process was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy terminal output&lt;/li&gt;
&lt;li&gt;Paste into &lt;a href="https://tools.simonwillison.net/rtf-to-html"&gt;rtf-to-html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Copy resulting HTML&lt;/li&gt;
&lt;li&gt;Paste that int a new GitHub Gist&lt;/li&gt;
&lt;li&gt;Grab that Gist's ID&lt;/li&gt;
&lt;li&gt;Share the link to &lt;code&gt;gitpreview.github.io?GIST_ID&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Not too much hassle, but frustratingly manual if you're doing it several times a day.&lt;/p&gt;
&lt;h4 id="the-desired-solution"&gt;The desired solution&lt;/h4&gt;
&lt;p&gt;Ideally I want a tool where I can do this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy terminal output&lt;/li&gt;
&lt;li&gt;Paste into a new tool&lt;/li&gt;
&lt;li&gt;Click a button and get a &lt;code&gt;gistpreview&lt;/code&gt; link to share&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I decided to get Claude Code for web to build the entire thing.&lt;/p&gt;
&lt;h4 id="the-prompt"&gt;The prompt&lt;/h4&gt;
&lt;p&gt;Here's the full prompt I used on &lt;a href="https://claude.ai/code"&gt;claude.ai/code&lt;/a&gt;, pointed at my &lt;code&gt;simonw/tools&lt;/code&gt; repo, to build the tool:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build a new tool called terminal-to-html which lets the user copy RTF directly from their terminal and paste it into a paste area, it then produces the HTML version of that in a textarea with a copy button, below is a button that says "Save this to a Gist", and below that is a full preview. It will be very similar to the existing rtf-to-html.html tool but it doesn't show the raw RTF and it has that Save this to a Gist button&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;That button should do the same trick that openai-audio-output.html does, with the same use of localStorage and the same flow to get users signed in with a token if they are not already&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;So click the button, it asks the user to sign in if necessary, then it saves that HTML to a Gist in a file called index.html, gets back the Gist ID and shows the user the URL https://gistpreview.github.io/?6d778a8f9c4c2c005a189ff308c3bc47 - but with their gist ID in it&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;They can see the URL, they can click it (do not use target="_blank") and there is also a "Copy URL" button to copy it to their clipboard&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Make the UI mobile friendly but also have it be courier green-text-on-black themed to reflect what it does&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;If the user pastes and the pasted data is available as HTML but not as RTF skip the RTF step and process the HTML directly&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;If the user pastes and it's only available as plain text then generate HTML that is just an open &amp;lt;pre&amp;gt; tag and their text and a closing &amp;lt;/pre&amp;gt; tag&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's quite a long prompt - it took me several minutes to type! But it covered the functionality I wanted in enough detail that I was pretty confident Claude would be able to build it.&lt;/p&gt;
&lt;h4 id="combining"&gt;Combining previous tools&lt;/h4&gt;
&lt;p&gt;I'm using one key technique in this prompt: I'm referencing existing tools in the same repo and telling Claude to imitate their functionality.&lt;/p&gt;
&lt;p&gt;I first wrote about this trick last March in &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/"&gt;Running OCR against PDFs and images directly in your browser&lt;/a&gt;, where I described how a snippet of code that used PDF.js and another snippet that used Tesseract.js was enough for Claude 3 Opus to build me this &lt;a href="https://tools.simonwillison.net/ocr"&gt;working PDF OCR tool&lt;/a&gt;. That was actually the tool that kicked off my &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; collection in the first place, which has since grown to 139 and counting.&lt;/p&gt;
&lt;p&gt;Here I'm telling Claude that I want the RTF to HTML functionality of &lt;a href="https://github.com/simonw/tools/blob/main/rtf-to-html.html"&gt;rtf-to-html.html&lt;/a&gt; combined with the Gist saving functionality of &lt;a href="https://github.com/simonw/tools/blob/main/openai-audio-output.html"&gt;openai-audio-output.html&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That one has quite a bit going on. It uses the OpenAI audio API to generate audio output from a text prompt, which is returned by that API as base64-encoded data in JSON.&lt;/p&gt;
&lt;p&gt;Then it offers the user a button to save that JSON to a Gist, which gives the snippet a URL.&lt;/p&gt;
&lt;p&gt;Another tool I wrote, &lt;a href="https://github.com/simonw/tools/blob/main/gpt-4o-audio-player.html"&gt;gpt-4o-audio-player.html&lt;/a&gt;, can then accept that Gist ID in the URL and will fetch the JSON data and make the audio playable in the browser. &lt;a href="https://tools.simonwillison.net/gpt-4o-audio-player?gist=4a982d3fe7ba8cb4c01e89c69a4a5335"&gt;Here's an example&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The trickiest part of this is API tokens. I've built tools in the past that require users to paste in a GitHub Personal Access Token (PAT) (which I then store in &lt;code&gt;localStorage&lt;/code&gt; in their browser - I don't want other people's authentication credentials anywhere near my own servers). But that's a bit fiddly.&lt;/p&gt;
&lt;p&gt;Instead, I &lt;a href="https://gist.github.com/simonw/975b8934066417fe771561a1b672ad4f"&gt;figured out&lt;/a&gt; the minimal Cloudflare worker necessary to implement the server-side portion of GitHub's authentication flow. That code &lt;a href="https://github.com/simonw/tools/blob/main/cloudflare-workers/github-auth.js"&gt;lives here&lt;/a&gt; and means that any of the HTML+JavaScript tools in my collection can implement a GitHub authentication flow if they need to save Gists.&lt;/p&gt;
&lt;p&gt;But I don't have to tell the model any of that! I can just say "do the same trick that openai-audio-output.html does" and Claude Code will work the rest out for itself.&lt;/p&gt;
&lt;h4 id="the-result"&gt;The result&lt;/h4&gt;
&lt;p&gt;Here's what &lt;a href="https://tools.simonwillison.net/terminal-to-html"&gt;the resulting app&lt;/a&gt; looks like after I've pasted in some terminal output from Claude Code CLI:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/terminal-to-html.jpg" alt="Terminal to HTML app. Green glowing text on black. Instructions: Paste terminal output below. Supports RTF, HTML or plain text. There's an HTML Code area with a Copy HTML button, Save this to a Gist and a bunch of HTML. Below is the result of save to a gist showing a URL and a Copy URL button. Below that a preview with the Claude Code heading in ASCII art." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It's exactly what I asked for, and the green-on-black terminal aesthetic is spot on too.&lt;/p&gt;
&lt;h4 id="other-notes-from-the-video"&gt;Other notes from the video&lt;/h4&gt;
&lt;p&gt;There are a bunch of other things that I touch on in the video. Here's a quick summary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tools.simonwillison.net/colophon"&gt;tools.simonwillison.net/colophon&lt;/a&gt; is the list of all of my tools, with accompanying AI-generated descriptions. Here's &lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#a-detailed-example"&gt;more about how I built that with Claude Code&lt;/a&gt; and notes on &lt;a href="https://simonwillison.net/2025/Mar/13/tools-colophon/"&gt;how I added the AI-generated descriptions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gistpreview.github.io"&gt;gistpreview.github.io&lt;/a&gt; is really neat.&lt;/li&gt;
&lt;li&gt;I used &lt;a href="https://www.descript.com/"&gt;Descript&lt;/a&gt; to record and edit the video. I'm still getting the hang of it - hence the slightly clumsy pan-and-zoom - but it's pretty great for this kind of screen recording.&lt;/li&gt;
&lt;li&gt;The site's automated deploys are managed &lt;a href="https://github.com/simonw/tools/blob/main/.github/workflows/pages.yml"&gt;by this GitHub Actions workflow&lt;/a&gt;. I also have it configured to work with &lt;a href="https://pages.cloudflare.com/"&gt;Cloudflare Pages&lt;/a&gt; for those preview deployments from PRs (here's &lt;a href="https://github.com/simonw/tools/pull/84#issuecomment-3434969331"&gt;an example&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The automated documentation is created using my &lt;a href="https://llm.datasette.io/"&gt;llm&lt;/a&gt; tool and &lt;a href="https://github.com/simonw/llm-anthropic"&gt;llm-anthropic&lt;/a&gt; plugin. Here's &lt;a href="https://github.com/simonw/tools/blob/main/write_docs.py"&gt;the script that does that&lt;/a&gt;, recently &lt;a href="https://github.com/simonw/tools/commit/99f5f2713f8001b72f4b1cafee5a15c0c26efb0d"&gt;upgraded&lt;/a&gt; to use Claude Haiku 4.5.&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/youtube"&gt;youtube&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;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/async-coding-agents"&gt;async-coding-agents&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="tools"/><category term="youtube"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="vibe-coding"/><category term="coding-agents"/><category term="claude-code"/><category term="async-coding-agents"/></entry><entry><title>Quoting Kenton Varda</title><link href="https://simonwillison.net/2025/Sep/5/kenton-varda/#atom-tag" rel="alternate"/><published>2025-09-05T16:43:13+00:00</published><updated>2025-09-05T16:43:13+00:00</updated><id>https://simonwillison.net/2025/Sep/5/kenton-varda/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://twitter.com/KentonVarda/status/1963966469148180839"&gt;&lt;p&gt;After struggling for years trying to figure out why people think [Cloudflare] Durable Objects are complicated, I'm increasingly convinced that it's just that they &lt;em&gt;sound&lt;/em&gt; complicated.&lt;/p&gt;
&lt;p&gt;Feels like we can solve 90% of it by renaming &lt;code&gt;DurableObject&lt;/code&gt; to &lt;code&gt;StatefulWorker&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;It's just a worker that has state. And because it has state, it also has to have a name, so that you can route to the specific worker that has the state you care about. There may be a sqlite database attached, there may be a container attached. Those are just part of the state.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://twitter.com/KentonVarda/status/1963966469148180839"&gt;Kenton Varda&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kenton-varda"&gt;kenton-varda&lt;/a&gt;&lt;/p&gt;



</summary><category term="sqlite"/><category term="cloudflare"/><category term="kenton-varda"/></entry><entry><title>Cloudflare Radar: AI Insights</title><link href="https://simonwillison.net/2025/Sep/1/cloudflare-radar-ai-insights/#atom-tag" rel="alternate"/><published>2025-09-01T17:06:56+00:00</published><updated>2025-09-01T17:06:56+00:00</updated><id>https://simonwillison.net/2025/Sep/1/cloudflare-radar-ai-insights/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://radar.cloudflare.com/ai-insights"&gt;Cloudflare Radar: AI Insights&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cloudflare launched this dashboard &lt;a href="https://blog.cloudflare.com/expanded-ai-insights-on-cloudflare-radar/"&gt;back in February&lt;/a&gt;, incorporating traffic analysis from Cloudflare's network along with insights from their popular 1.1.1.1 DNS service.&lt;/p&gt;
&lt;p&gt;I found this chart particularly interesting, showing which documented AI crawlers are most active collecting training data - lead by GPTBot, ClaudeBot and Meta-ExternalAgent:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Line chart showing HTTP traffic by bot over time from August 26 to September 1. HTTP traffic by bot - HTTP request trends for top five most active AI bots. Crawl purpose: Training. GPTBot 31.7% (orange line), ClaudeBot 27.1% (blue line), Meta-ExternalAgent 25.3% (light blue line), Bytespider 9.3% (yellow-green line), Applebot 5.2% (green line). Max scale shown on y-axis. X-axis shows dates: Tue, Aug 26, Wed, Aug 27, Thu, Aug 28, Fri, Aug 29, Sat, Aug 30, Sun, Aug 31, Mon, Sep 1. Top right shows Crawl purpose dropdown set to &amp;quot;Training&amp;quot; with X and checkmark buttons." src="https://static.simonwillison.net/static/2025/http-traffic-by-bot.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Cloudflare's DNS data also hints at the popularity of different services. ChatGPT holds the first place, which is unsurprising - but second place is a hotly contested race between Claude and Perplexity and #4/#5/#6 is contested by GitHub Copilot, Perplexity, and Codeium/Windsurf.&lt;/p&gt;
&lt;p&gt;Google Gemini comes in 7th, though since this is DNS based I imagine this is undercounting instances of Gemini on &lt;code&gt;google.com&lt;/code&gt; as opposed to &lt;code&gt;gemini.google.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Line chart showing generative AI services popularity rankings over time. Title: &amp;quot;Generative AI services popularity&amp;quot; with subtitle &amp;quot;Top 10 services based on 1.1.1.1 DNS resolver traffic&amp;quot; and question mark and share icons. Legend shows: ChatGPT/OpenAI (dark blue), Character.AI (light blue), Claude/Anthropic (orange), Perplexity (olive green), GitHub Copilot (green), Codeium/Windsurf AI (pink), Google Gemini (purple), QuillBot (red), Grok/xAI (brown), DeepSeek (yellow). Y-axis shows ranks #1-#10, X-axis shows dates from Mon, Aug 25 to Mon, Sep 1 (partially visible). ChatGPT maintains #1 position throughout. Other services show various ranking changes over the week-long period." src="https://static.simonwillison.net/static/2025/cloudflare-gen-ai.jpg" /&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/crawling"&gt;crawling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dns"&gt;dns&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;/p&gt;



</summary><category term="crawling"/><category term="dns"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/></entry><entry><title>ChatGPT agent's user-agent</title><link href="https://simonwillison.net/2025/Aug/4/chatgpt-agents-user-agent/#atom-tag" rel="alternate"/><published>2025-08-04T22:49:25+00:00</published><updated>2025-08-04T22:49:25+00:00</updated><id>https://simonwillison.net/2025/Aug/4/chatgpt-agents-user-agent/#atom-tag</id><summary type="html">
    &lt;p&gt;I was exploring how ChatGPT agent works today. I learned some interesting things about how it exposes its identity through HTTP headers, then made a huge blunder in thinking it was leaking its URLs to Bingbot and Yandex... but it turned out &lt;a href="https://simonwillison.net/2025/Aug/4/chatgpt-agents-agent/#cloudflare-crawler-hints"&gt;that was a Cloudflare feature&lt;/a&gt; that had nothing to do with ChatGPT.&lt;/p&gt;

&lt;p&gt;ChatGPT agent is the &lt;a href="https://openai.com/index/introducing-chatgpt-agent/"&gt;recently released&lt;/a&gt; (and confusingly named) ChatGPT feature that provides browser automation combined with terminal access as a feature of ChatGPT - replacing their previous &lt;a href="https://help.openai.com/en/articles/10421097-operator"&gt;Operator research preview&lt;/a&gt; which is scheduled for deprecation on August 31st.&lt;/p&gt;

&lt;h4 id="investigating-chatgpt-agent-s-user-agent"&gt;Investigating ChatGPT agent's user-agent&lt;/h4&gt;
&lt;p&gt;I decided to dig into how it works by creating a logged web URL endpoint using &lt;a href="https://simonwillison.net/2024/Aug/8/django-http-debug/"&gt;django-http-debug&lt;/a&gt;. Then I told ChatGPT agent mode to explore that new page:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/chatgpt-agent-url.jpg" alt="ChatGPT screenshot. My prompt was &amp;quot;Visit https://simonwillison.net/test-url-context and tell me what you see there&amp;quot; - it said &amp;quot;Worked for 15 seconds&amp;quot; with an arrow, then a screnshot of the webpage content showing &amp;quot;simonwillison.net&amp;quot; with a favicon, heading &amp;quot;This is a heading&amp;quot;, text &amp;quot;Text and text and more text.&amp;quot; and &amp;quot;this came from javascript&amp;quot;. The bot then responds with: The webpage displays a simple layout with a large heading at the top that reads “This is a heading.” Below it, there's a short paragraph that says “Text and text and more text.” A final line appears underneath saying “this came from javascript,” indicating that this last line was inserted via a script. The page contains no interactive elements or instructions—just these lines of plain text displayed on a white background." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;My logging captured these request headers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Via: 1.1 heroku-router
Host: simonwillison.net
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Cf-Ray: 96a0f289adcb8e8e-SEA
Cookie: cf_clearance=zzV8W...
Server: Heroku
Cdn-Loop: cloudflare; loops=1
Priority: u=0, i
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Signature: sig1=:1AxfqHocTf693inKKMQ7NRoHoWAZ9d/vY4D/FO0+MqdFBy0HEH3ZIRv1c3hyiTrzCvquqDC8eYl1ojcPYOSpCQ==:
Cf-Visitor: {"scheme":"https"}
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Cf-Ipcountry: US
X-Request-Id: 45ef5be4-ead3-99d5-f018-13c4a55864d3
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Accept-Encoding: gzip, br
Accept-Language: en-US,en;q=0.9
Signature-Agent: "https://chatgpt.com"
Signature-Input: sig1=("@authority" "@method" "@path" "signature-agent");created=1754340838;keyid="otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg";expires=1754344438;nonce="_8jbGwfLcgt_vUeiZQdWvfyIeh9FmlthEXElL-O2Rq5zydBYWivw4R3sV9PV-zGwZ2OEGr3T2Pmeo2NzmboMeQ";tag="web-bot-auth";alg="ed25519"
X-Forwarded-For: 2a09:bac5:665f:1541::21e:154, 172.71.147.183
X-Request-Start: 1754340840059
Cf-Connecting-Ip: 2a09:bac5:665f:1541::21e:154
Sec-Ch-Ua-Mobile: ?0
X-Forwarded-Port: 80
X-Forwarded-Proto: http
Sec-Ch-Ua-Platform: "Linux"
Upgrade-Insecure-Requests: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;strong&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36&lt;/strong&gt; user-agent header is the one used by the most recent Chrome on macOS - which is a little odd here as the &lt;strong&gt;Sec-Ch-Ua-Platform : "Linux"&lt;/strong&gt; indicates that the agent browser runs on Linux.&lt;/p&gt;
&lt;p&gt;At first glance it looks like ChatGPT is being dishonest here by not including its bot identity in the user-agent header. I thought for a moment it might be reflecting my own user-agent, but I'm using Firefox on macOS and it identified itself as Chrome.&lt;/p&gt;
&lt;p&gt;Then I spotted this header:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Signature-Agent: "https://chatgpt.com"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is accompanied by a much more complex header called &lt;strong&gt;Signature-Input&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Signature-Input: sig1=("@authority" "@method" "@path" "signature-agent");created=1754340838;keyid="otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg";expires=1754344438;nonce="_8jbGwfLcgt_vUeiZQdWvfyIeh9FmlthEXElL-O2Rq5zydBYWivw4R3sV9PV-zGwZ2OEGr3T2Pmeo2NzmboMeQ";tag="web-bot-auth";alg="ed25519"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a &lt;code&gt;Signature&lt;/code&gt; header too.&lt;/p&gt;
&lt;p&gt;These turn out to come from a relatively new web standard: &lt;a href="https://www.rfc-editor.org/rfc/rfc9421.html"&gt;RFC 9421 HTTP Message Signatures&lt;/a&gt;' published February 2024.&lt;/p&gt;
&lt;p&gt;The purpose of HTTP Message Signatures is to allow clients to include signed data about their request in a way that cannot be tampered with by intermediaries. The signature uses a public key that's provided by the following well-known endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://chatgpt.com/.well-known/http-message-signatures-directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add it all together and we now have a rock-solid way to identify traffic from ChatGPT agent: look for the &lt;code&gt;Signature-Agent: "https://chatgpt.com"&lt;/code&gt; header and confirm its value by checking the signature in the &lt;code&gt;Signature-Input&lt;/code&gt; and &lt;code&gt;Signature&lt;/code&gt; headers.&lt;/p&gt;
&lt;h4 id="and-then-came-the-crawlers"&gt;And then came Bingbot and Yandex&lt;/h4&gt;
&lt;p&gt;Just over a minute after it captured that request, my logging endpoint got another request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Via: 1.1 heroku-router
From: bingbot(at)microsoft.com
Host: simonwillison.net
Accept: */*
Cf-Ray: 96a0f4671d1fc3c6-SEA
Server: Heroku
Cdn-Loop: cloudflare; loops=1
Cf-Visitor: {"scheme":"https"}
User-Agent: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36
Cf-Ipcountry: US
X-Request-Id: 6214f5dc-a4ea-5390-1beb-f2d26eac5d01
Accept-Encoding: gzip, br
X-Forwarded-For: 207.46.13.9, 172.71.150.252
X-Request-Start: 1754340916429
Cf-Connecting-Ip: 207.46.13.9
X-Forwarded-Port: 80
X-Forwarded-Proto: http
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I pasted &lt;code&gt;207.46.13.9&lt;/code&gt; into Microsoft's &lt;a href="https://www.bing.com/toolbox/verify-bingbot-verdict"&gt;Verify Bingbot&lt;/a&gt; tool (after solving a particularly taxing CAPTCHA) and it confirmed that this was indeed a request from Bingbot.&lt;/p&gt;
&lt;p&gt;I set up a second URL to confirm... and this time got a visit from Yandex!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Via: 1.1 heroku-router
From: support@search.yandex.ru
Host: simonwillison.net
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Cf-Ray: 96a16390d8f6f3a7-DME
Server: Heroku
Cdn-Loop: cloudflare; loops=1
Cf-Visitor: {"scheme":"https"}
User-Agent: Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)
Cf-Ipcountry: RU
X-Request-Id: 3cdcbdba-f629-0d29-b453-61644da43c6c
Accept-Encoding: gzip, br
X-Forwarded-For: 213.180.203.138, 172.71.184.65
X-Request-Start: 1754345469921
Cf-Connecting-Ip: 213.180.203.138
X-Forwarded-Port: 80
X-Forwarded-Proto: http
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yandex &lt;a href="https://yandex.com/support/webmaster/en/robot-workings/check-yandex-robots.html?lang=en"&gt;suggest a reverse DNS lookup&lt;/a&gt; to verify, so I ran this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dig -x 213.180.203.138 +short
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And got back:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;213-180-203-138.spider.yandex.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which confirms that this is indeed a Yandex crawler.&lt;/p&gt;

&lt;p&gt;I tried a third experiment to be sure... and got hits from both Bingbot and YandexBot.&lt;/p&gt;

&lt;h4 id="cloudflare-crawler-hints"&gt;It was Cloudflare Crawler Hints, not ChatGPT&lt;/h4&gt;

&lt;p&gt;So I wrote up and posted about my discovery... and &lt;a href="https://x.com/jatan_loya/status/1952506398270767499"&gt;Jatan Loya asked:&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;do you have crawler hints enabled in cf?&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;And yeah, it turned out I did. I spotted this in my caching configuration page (and it looks like I must have turned it on myself at some point in the past):&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/cloudflare-crawler-hints.jpg" alt="Screenshot of Cloudflare settings panel showing &amp;quot;Crawler Hints Beta&amp;quot; with description text explaining that Crawler Hints provide high quality data to search engines and other crawlers when sites using Cloudflare change their content. This allows crawlers to precisely time crawling, avoid wasteful crawls, and generally reduce resource consumption on origins and other Internet infrastructure. Below states &amp;quot;By enabling this service, you agree to share website information required for feature functionality and agree to the Supplemental Terms for Crawler Hints.&amp;quot; There is a toggle switch in the on position on the right side and a &amp;quot;Help&amp;quot; link in the bottom right corner." style="max-width: 100%" /&gt;&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://developers.cloudflare.com/cache/advanced-configuration/crawler-hints/"&gt;the Cloudflare documentation for that feature&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I deleted my posts on Twitter and Bluesky (since you can't edit those and I didn't want the misinformation to continue to spread) and edited &lt;a href="https://fedi.simonwillison.net/@simon/114972968822349077"&gt;my post on Mastodon&lt;/a&gt;, then updated this entry with the real reason this had happened.&lt;/p&gt;

&lt;p&gt;I also changed the URL of this entry as it turned out Twitter and Bluesky were caching my social media preview for the previous one, which included the incorrect information in the title.&lt;/p&gt;

&lt;details&gt;&lt;summary&gt;Original "So what's going on here?" section from my post&lt;/summary&gt;

&lt;p&gt;&lt;em&gt;Here's a section of my original post with my theories about what was going on before learning about Cloudflare Crawler Hints.&lt;/em&gt;&lt;/p&gt;

&lt;h4 id="so-what-s-going-on-here-"&gt;So what's going on here?&lt;/h4&gt;
&lt;p&gt;There are quite a few different moving parts here.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I'm using Firefox on macOS with the 1Password and Readwise Highlighter extensions installed and active. Since I didn't visit the debug pages at all with my own browser I don't think any of these are relevant to these results.&lt;/li&gt;
&lt;li&gt;ChatGPT agent makes just a single request to my debug URL ...&lt;/li&gt;
&lt;li&gt;... which is proxied through both Cloudflare and Heroku.&lt;/li&gt;
&lt;li&gt;Within about a minute, I get hits from one or both of Bingbot and Yandex.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Presumably ChatGPT agent itself is running behind at least one proxy - I would expect OpenAI to keep a close eye on that traffic to ensure it doesn't get abused.&lt;/p&gt;
&lt;p&gt;I'm guessing that infrastructure is hosted by Microsoft Azure. The &lt;a href="https://openai.com/policies/sub-processor-list/"&gt;OpenAI Sub-processor List&lt;/a&gt; - though that lists Microsoft Corporation, CoreWeave Inc, Oracle Cloud Platform and Google Cloud Platform under the "Cloud infrastructure" section so it could be any of those.&lt;/p&gt;
&lt;p&gt;Since the page is served over HTTPS my guess is that any intermediary proxies should be unable to see the path component of the URL, making the mystery of how Bingbot and Yandex saw the URL even more intriguing.&lt;/p&gt;
&lt;/details&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/bing"&gt;bing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/privacy"&gt;privacy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/search-engines"&gt;search-engines&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/user-agents"&gt;user-agents&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/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browser-agents"&gt;browser-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/retractions"&gt;retractions&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="bing"/><category term="privacy"/><category term="search-engines"/><category term="user-agents"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="browser-agents"/><category term="retractions"/></entry><entry><title>TIL: Rate limiting by IP using Cloudflare's rate limiting rules</title><link href="https://simonwillison.net/2025/Jul/3/rate-limiting-by-ip/#atom-tag" rel="alternate"/><published>2025-07-03T21:16:51+00:00</published><updated>2025-07-03T21:16:51+00:00</updated><id>https://simonwillison.net/2025/Jul/3/rate-limiting-by-ip/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/cloudflare/rate-limiting"&gt;TIL: Rate limiting by IP using Cloudflare&amp;#x27;s rate limiting rules&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My blog started timing out on some requests a few days ago, and it turned out there were misbehaving crawlers that were spidering my &lt;code&gt;/search/&lt;/code&gt; page even though it's restricted by &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I run this site behind Cloudflare and it turns out Cloudflare's WAF (Web Application Firewall) has a rate limiting tool that I could use to restrict requests to &lt;code&gt;/search/*&lt;/code&gt; by a specific IP to a maximum of 5 every 10 seconds.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/rate-limiting"&gt;rate-limiting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/til"&gt;til&lt;/a&gt;&lt;/p&gt;



</summary><category term="rate-limiting"/><category term="security"/><category term="cloudflare"/><category term="til"/></entry><entry><title>New sandboxes from Cloudflare and Vercel</title><link href="https://simonwillison.net/2025/Jun/26/sandboxes/#atom-tag" rel="alternate"/><published>2025-06-26T01:41:32+00:00</published><updated>2025-06-26T01:41:32+00:00</updated><id>https://simonwillison.net/2025/Jun/26/sandboxes/#atom-tag</id><summary type="html">
    &lt;p&gt;Two interesting new products for running code in a sandbox today.&lt;/p&gt;
&lt;p&gt;Cloudflare &lt;a href="https://blog.cloudflare.com/containers-are-available-in-public-beta-for-simple-global-and-programmable/"&gt;launched their Containers product&lt;/a&gt; in open beta, and added &lt;a href="https://developers.cloudflare.com/changelog/2025-06-24-announcing-sandboxes/"&gt;a new Sandbox library&lt;/a&gt; for Cloudflare Workers that can run commands in a "secure, container-based environment":&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s1"&gt;getSandbox&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"@cloudflare/sandbox"&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;sandbox&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;getSandbox&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;env&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Sandbox&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"my-sandbox"&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;output&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sandbox&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;exec&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"ls"&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-s"&gt;"-la"&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;Vercel shipped a similar feature, introduced in &lt;a href="https://vercel.com/changelog/run-untrusted-code-with-vercel-sandbox"&gt;Run untrusted code with Vercel Sandbox&lt;/a&gt;, which enables code that looks like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;Sandbox&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"@vercel/sandbox"&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;sandbox&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-v"&gt;Sandbox&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&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-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;sandbox&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;writeFiles&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;path&lt;/span&gt;: &lt;span class="pl-s"&gt;"script.js"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;stream&lt;/span&gt;: &lt;span class="pl-v"&gt;Buffer&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;from&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;result&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&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-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;sandbox&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;runCommand&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;cmd&lt;/span&gt;: &lt;span class="pl-s"&gt;"node"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;args&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"script.js"&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;stdout&lt;/span&gt;: &lt;span class="pl-s1"&gt;process&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;stdout&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;stderr&lt;/span&gt;: &lt;span class="pl-s1"&gt;process&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;stderr&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;In both cases a major intended use-case is safely executing code that has been created by an LLM.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/vercel"&gt;vercel&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/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;&lt;/p&gt;



</summary><category term="vercel"/><category term="cloudflare"/><category term="generative-ai"/><category term="ai"/><category term="llms"/><category term="sandboxing"/></entry><entry><title>Cloudflare Project Galileo</title><link href="https://simonwillison.net/2025/Jun/16/cloudflare-project-galileo/#atom-tag" rel="alternate"/><published>2025-06-16T19:13:48+00:00</published><updated>2025-06-16T19:13:48+00:00</updated><id>https://simonwillison.net/2025/Jun/16/cloudflare-project-galileo/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.cloudflare.com/galileo/"&gt;Cloudflare Project Galileo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I only just heard about this Cloudflare initiative, though it's been around for more than a decade:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are an organization working in human rights, civil society, journalism, or democracy, you can apply for Project Galileo to get free cyber security protection from Cloudflare.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's effectively free denial-of-service protection for vulnerable targets in the civil rights public interest groups.&lt;/p&gt;
&lt;p&gt;Last week they published &lt;a href="https://blog.cloudflare.com/celebrating-11-years-of-project-galileo-global-impact/"&gt;Celebrating 11 years of Project Galileo’s global impact&lt;/a&gt; with some noteworthy numbers:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Journalists and news organizations experienced the highest volume of attacks, with over 97 billion requests blocked as potential threats across 315 different organizations. [...]&lt;/p&gt;
&lt;p&gt;Cloudflare onboarded the &lt;a href="https://investigatebel.org/en"&gt;Belarusian Investigative Center&lt;/a&gt;, an independent journalism organization, on September 27, 2024, while it was already under attack. A major application-layer DDoS attack followed on September 28, generating over 28 billion requests in a single day.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/denial-of-service"&gt;denial-of-service&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/journalism"&gt;journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;



</summary><category term="denial-of-service"/><category term="journalism"/><category term="security"/><category term="cloudflare"/></entry><entry><title>Quoting Kenton Varda</title><link href="https://simonwillison.net/2025/Jun/2/kenton-varda/#atom-tag" rel="alternate"/><published>2025-06-02T18:52:01+00:00</published><updated>2025-06-02T18:52:01+00:00</updated><id>https://simonwillison.net/2025/Jun/2/kenton-varda/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=44159166#44160208"&gt;&lt;p&gt;It took me a few days to build the library [&lt;a href="https://github.com/cloudflare/workers-oauth-provider"&gt;cloudflare/workers-oauth-provider&lt;/a&gt;] with AI.&lt;/p&gt;
&lt;p&gt;I estimate it would have taken a few weeks, maybe months to write by hand.&lt;/p&gt;
&lt;p&gt;That said, this is a pretty ideal use case: implementing a well-known standard on a well-known platform with a clear API spec.&lt;/p&gt;
&lt;p&gt;In my attempts to make changes to the Workers Runtime itself using AI, I've generally not felt like it saved much time. Though, people who don't know the codebase as well as I do have reported it helped them a lot.&lt;/p&gt;
&lt;p&gt;I have found AI incredibly useful when I jump into &lt;em&gt;other people's&lt;/em&gt; complex codebases, that I'm not familiar with. I now feel like I'm comfortable doing that, since AI can help me find my way around very quickly, whereas previously I generally shied away from jumping in and would instead try to get someone on the team to make whatever change I needed.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=44159166#44160208"&gt;Kenton Varda&lt;/a&gt;, in a Hacker News comment&lt;/p&gt;

    &lt;p&gt;Tags: &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;a href="https://simonwillison.net/tags/kenton-varda"&gt;kenton-varda&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="kenton-varda"/></entry><entry><title>llm-prices.com</title><link href="https://simonwillison.net/2025/May/7/llm-prices/#atom-tag" rel="alternate"/><published>2025-05-07T20:15:48+00:00</published><updated>2025-05-07T20:15:48+00:00</updated><id>https://simonwillison.net/2025/May/7/llm-prices/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.llm-prices.com/"&gt;llm-prices.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've been maintaining a simple LLM pricing calculator since &lt;a href="https://github.com/simonw/tools/commits/main/llm-prices.html"&gt;October last year&lt;/a&gt;. I finally decided to split it out to its own domain name (previously it was hosted at &lt;code&gt;tools.simonwillison.net/llm-prices&lt;/code&gt;), running on Cloudflare Pages.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the llm-prices.com site - on the left is a calculator interface for entering number of input tokens, output tokens and price per million of each. On the right is a table of models and their prices, sorted cheapest first." src="https://static.simonwillison.net/static/2025/llm-prices.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;The site runs out of my &lt;a href="https://github.com/simonw/llm-prices"&gt;simonw/llm-prices&lt;/a&gt; GitHub repository. I ported &lt;a href="https://github.com/simonw/llm-prices/commits/b45e8f9c718c4ad3ab50b906a2c3882cbcffcb5b/index.html"&gt;the history&lt;/a&gt; of the old &lt;code&gt;llm-prices.html&lt;/code&gt; file using a vibe-coded bash script that I forgot to save anywhere.&lt;/p&gt;
&lt;p&gt;I rarely use AI-generated imagery in my own projects, but for this one I found an excellent reason to use GPT-4o image outputs... to generate the favicon! I dropped a screenshot of the site into ChatGPT (o4-mini-high in this case) and asked for the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;design a bunch of options for favicons for this site in a single image, white background&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="A 3x3 grid of simple icon concepts: green coins/circles, a green price tag with dollar sign, a calculator with dollar sign, a calculator with plus sign, a blue chat bubble with three dots, a green brain icon, the letters &amp;quot;AI&amp;quot; in dark gray, a document with finger pointing at it, and green horizontal bars of decreasing size." src="https://static.simonwillison.net/static/2025/favicon-options.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I liked the top right one, so I cropped it into Pixelmator and made a 32x32 version. Here's what it looks like in my browser:&lt;/p&gt;
&lt;p&gt;&lt;img alt="A cropped web browser showing the chosen favicon - it's a calculator with a dollar sign overlapping some of the keys." src="https://static.simonwillison.net/static/2025/favicon-live.png" /&gt;&lt;/p&gt;
&lt;p&gt;I added a new feature just now: the state of the calculator is now reflected in the &lt;code&gt;#fragment-hash&lt;/code&gt; URL of the page, which means you can link to your previous calculations.&lt;/p&gt;
&lt;p&gt;I implemented that feature using &lt;a href="https://simonwillison.net/2025/May/6/gemini-25-pro-preview/"&gt;the new gemini-2.5-pro-preview-05-06&lt;/a&gt;, since that model boasts improved front-end coding abilities. It did a pretty great job - here's how I prompted it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm -m gemini-2.5-pro-preview-05-06 -f https://www.llm-prices.com/ -s 'modify this code so that the state of the page is reflected in the fragmenth hash URL - I want to capture the values filling out the form fields and also the current sort order of the table. These should be respected when the page first loads too. Update them using replaceHistory, no need to enable the back button.'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/9d4e15b58ccfaca9e08747225cb69fa2"&gt;the transcript&lt;/a&gt; and &lt;a href="https://github.com/simonw/llm-prices/commit/c9eee704d070d119e6c342d9a7ab6c41d09550dd"&gt;the commit updating the tool&lt;/a&gt;, plus &lt;a href="https://www.llm-prices.com/#it=5883&amp;amp;ot=16087&amp;amp;ic=1.25&amp;amp;oc=10&amp;amp;sb=input&amp;amp;sd=descending"&gt;an example link&lt;/a&gt; showing the new feature in action (and calculating the cost for that Gemini 2.5 Pro prompt at 16.8224 cents, after &lt;a href="https://simonwillison.net/2025/May/8/llm-gemini-0191/"&gt;fixing the calculation&lt;/a&gt;.)


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/favicons"&gt;favicons&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-pricing"&gt;llm-pricing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/text-to-image"&gt;text-to-image&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;&lt;/p&gt;



</summary><category term="favicons"/><category term="projects"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="gemini"/><category term="llm-pricing"/><category term="text-to-image"/><category term="vibe-coding"/></entry><entry><title>Note on 18th April 2025</title><link href="https://simonwillison.net/2025/Apr/18/link-to-things/#atom-tag" rel="alternate"/><published>2025-04-18T23:59:01+00:00</published><updated>2025-04-18T23:59:01+00:00</updated><id>https://simonwillison.net/2025/Apr/18/link-to-things/#atom-tag</id><summary type="html">
    &lt;p&gt;It frustrates me when support sites for online services fail to &lt;em&gt;link&lt;/em&gt; to the things they are talking about. Cloudflare's &lt;a href="https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/"&gt;Find zone and account IDs&lt;/a&gt; page for example provides a four step process for finding my account ID that starts at the root of their dashboard, including a screenshot of where I should click.&lt;/p&gt;
&lt;p&gt;&lt;img alt="1. Log in to the Cloudflare dashboard. 2. Select your account and domain. 3. On the Overview page (the landing page for your domain), find the API section. Screenshot includes an Overview panel showing analytics" src="https://static.simonwillison.net/static/2025/cloudflare-docs.jpg" style="max-width: 100%"&gt;&lt;/p&gt;
&lt;p&gt;In Cloudflare's case it's harder to link to the correct dashboard page because the URL differs for different users, but that shouldn't be a show-stopper for getting this to work. Set up &lt;code&gt;dash.cloudflare.com/redirects/find-account-id&lt;/code&gt; and link to that!&lt;/p&gt;
&lt;p&gt;... I just noticed they &lt;em&gt;do&lt;/em&gt; have a mechanism like that which they use elsewhere. On the &lt;a href="https://developers.cloudflare.com/r2/api/tokens/"&gt;R2 authentication page&lt;/a&gt; they link to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://dash.cloudflare.com/?to=/:account/r2/api-tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The "find account ID" flow presumably can't do the same thing because there is no single page displaying that information - it's shown in a sidebar on the page for each of your Cloudflare domains.&lt;/p&gt;

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



</summary><category term="urls"/><category term="usability"/><category term="cloudflare"/></entry><entry><title>OpenTimes</title><link href="https://simonwillison.net/2025/Mar/17/opentimes/#atom-tag" rel="alternate"/><published>2025-03-17T22:49:59+00:00</published><updated>2025-03-17T22:49:59+00:00</updated><id>https://simonwillison.net/2025/Mar/17/opentimes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://sno.ws/opentimes/"&gt;OpenTimes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Spectacular new open geospatial project by &lt;a href="https://sno.ws/"&gt;Dan Snow&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenTimes is a database of pre-computed, point-to-point travel times between United States Census geographies. It lets you download bulk travel time data for free and with no limits.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://opentimes.org/?id=060816135022&amp;amp;mode=car#9.76/37.5566/-122.3085"&gt;what I get&lt;/a&gt; for travel times by car from El Granada, California:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Isochrone map showing driving times from the El Granada census tract to other places in the San Francisco Bay Area" src="https://static.simonwillison.net/static/2025/opentimes.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;The technical details are &lt;em&gt;fascinating&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The entire OpenTimes backend is just static Parquet files on &lt;a href="https://www.cloudflare.com/developer-platform/products/r2/"&gt;Cloudflare's R2&lt;/a&gt;. There's no RDBMS or running service, just files and a CDN. The whole thing costs about $10/month to host and costs nothing to serve. In my opinion, this is a &lt;em&gt;great&lt;/em&gt; way to serve infrequently updated, large public datasets at low cost (as long as you partition the files correctly).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sure enough, &lt;a href="https://developers.cloudflare.com/r2/pricing/"&gt;R2 pricing&lt;/a&gt; charges "based on the total volume of data stored" - $0.015 / GB-month for standard storage, then $0.36 / million requests for "Class B" operations which include reads. They charge nothing for outbound bandwidth.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;All travel times were calculated by pre-building the inputs (OSM, OSRM networks) and then distributing the compute over &lt;a href="https://github.com/dfsnow/opentimes/actions/workflows/calculate-times.yaml"&gt;hundreds of GitHub Actions jobs&lt;/a&gt;. This worked shockingly well for this specific workload (and was also completely free).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's a &lt;a href="https://github.com/dfsnow/opentimes/actions/runs/13094249792"&gt;GitHub Actions run&lt;/a&gt; of the &lt;a href="https://github.com/dfsnow/opentimes/blob/a6a5f7abcdd69559b3e29f360fe0ff0399dbb400/.github/workflows/calculate-times.yaml#L78-L80"&gt;calculate-times.yaml workflow&lt;/a&gt; which uses a matrix to run 255 jobs!&lt;/p&gt;
&lt;p&gt;&lt;img alt="GitHub Actions run: calculate-times.yaml run by workflow_dispatch taking 1h49m to execute 255 jobs with names like run-job (2020-01) " src="https://static.simonwillison.net/static/2025/opentimes-github-actions.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Relevant YAML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  matrix:
    year: ${{ fromJSON(needs.setup-jobs.outputs.years) }}
    state: ${{ fromJSON(needs.setup-jobs.outputs.states) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where those JSON files were created by the previous step, which reads in the year and state values from &lt;a href="https://github.com/dfsnow/opentimes/blob/a6a5f7abcdd69559b3e29f360fe0ff0399dbb400/data/params.yaml#L72-L132"&gt;this params.yaml file&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The query layer uses a single DuckDB database file with &lt;em&gt;views&lt;/em&gt; that point to static Parquet files via HTTP. This lets you query a table with hundreds of billions of records after downloading just the ~5MB pointer file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a really creative use of DuckDB's feature that lets you run queries against large data from a laptop using HTTP range queries to avoid downloading the whole thing.&lt;/p&gt;
&lt;p&gt;The README shows &lt;a href="https://github.com/dfsnow/opentimes/blob/3439fa2c54af227e40997b4a5f55678739e0f6df/README.md#using-duckdb"&gt;how to use that from R and Python&lt;/a&gt; - I got this working in the &lt;code&gt;duckdb&lt;/code&gt; client (&lt;code&gt;brew install duckdb&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALL httpfs;
LOAD httpfs;
ATTACH 'https://data.opentimes.org/databases/0.0.1.duckdb' AS opentimes;

SELECT origin_id, destination_id, duration_sec
  FROM opentimes.public.times
  WHERE version = '0.0.1'
      AND mode = 'car'
      AND year = '2024'
      AND geography = 'tract'
      AND state = '17'
      AND origin_id LIKE '17031%' limit 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In answer to a question about adding public transit times &lt;a href="https://news.ycombinator.com/item?id=43392521#43393183"&gt;Dan said&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the next year or so maybe. The biggest obstacles to adding public transit are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collecting all the necessary scheduling data (e.g. GTFS feeds) for every transit system in the county. Not insurmountable since there are services that do this currently.&lt;/li&gt;
&lt;li&gt;Finding a routing engine that can compute nation-scale travel time matrices quickly. Currently, the two fastest open-source engines I've tried (OSRM and Valhalla) don't support public transit for matrix calculations and the engines that do support public transit (R5, OpenTripPlanner, etc.) are too slow.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://gtfs.org/"&gt;GTFS&lt;/a&gt; is a popular CSV-based format for sharing transit schedules - here's &lt;a href="https://gtfs.org/resources/data/"&gt;an official list&lt;/a&gt; of available feed directories.&lt;/p&gt;
&lt;p&gt;This whole project feels to me like a great example of the &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/"&gt;baked data&lt;/a&gt; architectural pattern in action.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/census"&gt;census&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-data"&gt;open-data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/parquet"&gt;parquet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/baked-data"&gt;baked-data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duckdb"&gt;duckdb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;



</summary><category term="census"/><category term="geospatial"/><category term="open-data"/><category term="openstreetmap"/><category term="cloudflare"/><category term="parquet"/><category term="github-actions"/><category term="baked-data"/><category term="duckdb"/><category term="http-range-requests"/></entry><entry><title>OpenAI WebRTC Audio demo</title><link href="https://simonwillison.net/2024/Dec/17/openai-webrtc/#atom-tag" rel="alternate"/><published>2024-12-17T23:50:12+00:00</published><updated>2024-12-17T23:50:12+00:00</updated><id>https://simonwillison.net/2024/Dec/17/openai-webrtc/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/openai-webrtc"&gt;OpenAI WebRTC Audio demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
OpenAI announced &lt;a href="https://openai.com/index/o1-and-new-tools-for-developers/"&gt;a bunch of API features&lt;/a&gt; today, including a brand new &lt;a href="https://platform.openai.com/docs/guides/realtime-webrtc"&gt;WebRTC API&lt;/a&gt; for setting up a two-way audio conversation with their models.&lt;/p&gt;
&lt;p&gt;They &lt;a href="https://twitter.com/OpenAIDevs/status/1869116585044259059"&gt;tweeted this opaque code example&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;async function createRealtimeSession(inStream, outEl, token) {
const pc = new RTCPeerConnection();
pc.ontrack = e =&amp;gt; outEl.srcObject = e.streams[0];
pc.addTrack(inStream.getTracks()[0]);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const headers = { Authorization: &lt;code&gt;Bearer ${token}&lt;/code&gt;, 'Content-Type': 'application/sdp' };
const opts = { method: 'POST', body: offer.sdp, headers };
const resp = await fetch('https://api.openai.com/v1/realtime', opts);
await pc.setRemoteDescription({ type: 'answer', sdp: await resp.text() });
return pc;
}&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I &lt;a href="https://gist.github.com/simonw/69151091f7672adb9b42f5b17bd45d44"&gt;pasted that into Claude&lt;/a&gt; and had it build me &lt;a href="https://tools.simonwillison.net/openai-webrtc"&gt;this interactive demo&lt;/a&gt; for trying out the new API.&lt;/p&gt;
&lt;div style="max-width: 100%; margin: 1em 0"&gt;
    &lt;video 
        controls 
        preload="none"
        poster="https://static.simonwillison.net/static/2024/webrtc-demo.jpg" loop
        style="width: 100%; height: auto;"&gt;
        &lt;source src="https://static.simonwillison.net/static/2024/webrtc-demo.mp4" type="video/mp4"&gt;
    &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;My demo uses an OpenAI key directly, but the most interesting aspect of the new WebRTC mechanism is its support for &lt;a href="https://platform.openai.com/docs/guides/realtime-webrtc#creating-an-ephemeral-token"&gt;ephemeral tokens&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This solves a major problem with their previous realtime API: in order to connect to their endpoint you need to provide an API key, but that meant making that key visible to anyone who uses your application. The only secure way to handle this was to roll a full server-side proxy for their WebSocket API, just so you could hide your API key in your own server. &lt;a href="https://github.com/cloudflare/openai-workers-relay"&gt;cloudflare/openai-workers-relay&lt;/a&gt; is an example implementation of that pattern.&lt;/p&gt;
&lt;p&gt;Ephemeral tokens solve that by letting you make a server-side call to request an ephemeral token which will only allow a connection to be initiated to their WebRTC endpoint for the next 60 seconds. The user's browser then starts the connection, which will last for up to 30 minutes.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/audio"&gt;audio&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&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/openai"&gt;openai&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;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/multi-modal-output"&gt;multi-modal-output&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="audio"/><category term="security"/><category term="tools"/><category term="ai"/><category term="cloudflare"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude"/><category term="multi-modal-output"/></entry><entry><title>GitHub OAuth for a static site using Cloudflare Workers</title><link href="https://simonwillison.net/2024/Nov/29/github-oauth-cloudflare/#atom-tag" rel="alternate"/><published>2024-11-29T18:13:18+00:00</published><updated>2024-11-29T18:13:18+00:00</updated><id>https://simonwillison.net/2024/Nov/29/github-oauth-cloudflare/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/cloudflare/workers-github-oauth"&gt;GitHub OAuth for a static site using Cloudflare Workers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's a TIL covering a Thanksgiving AI-assisted programming project. I wanted to add OAuth against GitHub to some of the projects on my &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; site in order to implement "Save to Gist".&lt;/p&gt;
&lt;p&gt;That site is entirely statically hosted by GitHub Pages, but OAuth has a required server-side component: there's a &lt;code&gt;client_secret&lt;/code&gt; involved that should never be included in client-side code.&lt;/p&gt;
&lt;p&gt;Since I serve the site from behind Cloudflare I realized that a minimal &lt;a href="https://workers.cloudflare.com/"&gt;Cloudflare Workers&lt;/a&gt; script may be enough to plug the gap. I got Claude on my phone to build me a prototype and then pasted that (still on my phone) into a new Cloudflare Worker and it worked!&lt;/p&gt;
&lt;p&gt;... almost. On later closer inspection of the code it was missing error handling... and then someone pointed out it was vulnerable to a login CSRF attack thanks to failure to check the &lt;code&gt;state=&lt;/code&gt; parameter. I worked with Claude to fix those too.&lt;/p&gt;
&lt;p&gt;Useful reminder here that pasting code AI-generated code around on a mobile phone isn't necessarily the best environment to encourage a thorough code review!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/oauth"&gt;oauth&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&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="csrf"/><category term="github"/><category term="oauth"/><category term="projects"/><category term="security"/><category term="tools"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>Zero-latency SQLite storage in every Durable Object</title><link href="https://simonwillison.net/2024/Oct/13/zero-latency-sqlite-storage-in-every-durable-object/#atom-tag" rel="alternate"/><published>2024-10-13T22:26:49+00:00</published><updated>2024-10-13T22:26:49+00:00</updated><id>https://simonwillison.net/2024/Oct/13/zero-latency-sqlite-storage-in-every-durable-object/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.cloudflare.com/sqlite-in-durable-objects/"&gt;Zero-latency SQLite storage in every Durable Object&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Kenton Varda introduces the next iteration of Cloudflare's &lt;a href="https://developers.cloudflare.com/durable-objects/"&gt;Durable Object&lt;/a&gt; platform, which recently upgraded from a key/value store to a full relational system based on SQLite.&lt;/p&gt;
&lt;p&gt;For useful background on the first version of Durable Objects take a look at &lt;a href="https://digest.browsertech.com/archive/browsertech-digest-cloudflares-durable/"&gt;Cloudflare's durable multiplayer moat&lt;/a&gt; by Paul Butler, who digs into its popularity for building WebSocket-based realtime collaborative applications.&lt;/p&gt;
&lt;p&gt;The new SQLite-backed Durable Objects is a fascinating piece of distributed system design, which advocates for a really interesting way to architect a large scale application.&lt;/p&gt;
&lt;p&gt;The key idea behind Durable Objects is to colocate application logic with the data it operates on. A Durable Object comprises code that executes on the same physical host as the SQLite database that it uses, resulting in blazingly fast read and write performance.&lt;/p&gt;
&lt;p&gt;How could this work at scale?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A single object is inherently limited in throughput since it runs on a single thread of a single machine. To handle more traffic, you create more objects. This is easiest when different objects can handle different logical units of state (like different documents, different users, or different "shards" of a database), where each unit of state has low enough traffic to be handled by a single object&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kenton presents the example of a flight booking system, where each flight can map to a dedicated Durable Object with its own SQLite database - thousands of fresh databases per airline per day.&lt;/p&gt;
&lt;p&gt;Each DO has a unique name, and Cloudflare's network then handles routing requests to that object wherever it might live on their global network.&lt;/p&gt;
&lt;p&gt;The technical details are fascinating. Inspired by &lt;a href="https://litestream.io/"&gt;Litestream&lt;/a&gt;, each DO constantly streams a sequence of WAL entries to object storage - batched every 16MB or every ten seconds. This also enables point-in-time recovery for up to 30 days through replaying those logged transactions.&lt;/p&gt;
&lt;p&gt;To ensure durability within that ten second window, writes are also forwarded to five replicas in separate nearby data centers as soon as they commit, and the write is only acknowledged once three of them have confirmed it.&lt;/p&gt;
&lt;p&gt;The JavaScript API design is interesting too: it's blocking rather than async, because the whole point of the design is to provide fast single threaded persistence operations:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;docs&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;exec&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;
&lt;span class="pl-s"&gt;  SELECT title, authorId FROM documents&lt;/span&gt;
&lt;span class="pl-s"&gt;  ORDER BY lastModified DESC&lt;/span&gt;
&lt;span class="pl-s"&gt;  LIMIT 100&lt;/span&gt;
&lt;span class="pl-s"&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-en"&gt;toArray&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-k"&gt;for&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;doc&lt;/span&gt; &lt;span class="pl-k"&gt;of&lt;/span&gt; &lt;span class="pl-s1"&gt;docs&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;doc&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;authorName&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;exec&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s"&gt;"SELECT name FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-s1"&gt;doc&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;authorId&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;one&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;name&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;This one of their examples deliberately exhibits the N+1 query pattern, because that's something SQLite is &lt;a href="https://www.sqlite.org/np1queryprob.html"&gt;uniquely well suited to handling&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The system underlying Durable Objects is called Storage Relay Service, and it's been powering Cloudflare's existing-but-different &lt;a href="https://developers.cloudflare.com/d1/"&gt;D1 SQLite system&lt;/a&gt; for over a year.&lt;/p&gt;
&lt;p&gt;I was curious as to where the objects are created. &lt;a href="https://developers.cloudflare.com/durable-objects/reference/data-location/#provide-a-location-hint"&gt;According to this&lt;/a&gt; (via &lt;a href="https://news.ycombinator.com/item?id=41832547#41832812"&gt;Hacker News&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Durable Objects do not currently change locations after they are created. By default, a Durable Object is instantiated in a data center close to where the initial &lt;code&gt;get()&lt;/code&gt; request is made. [...] To manually create Durable Objects in another location, provide an optional &lt;code&gt;locationHint&lt;/code&gt; parameter to &lt;code&gt;get()&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And in a footnote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Dynamic relocation of existing Durable Objects is planned for the future.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://where.durableobjects.live/"&gt;where.durableobjects.live&lt;/a&gt; is a neat site that tracks where in the Cloudflare network DOs are created - I just visited it and it said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This page tracks where new Durable Objects are created; for example, when you loaded this page from &lt;strong&gt;Half Moon Bay&lt;/strong&gt;, a worker in &lt;strong&gt;San Jose, California, United States (SJC)&lt;/strong&gt; created a durable object in &lt;strong&gt;San Jose, California, United States (SJC)&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Where Durable Objects Live.    Created by the wonderful Jed Schmidt, and now maintained with ❤️ by Alastair. Source code available on Github.    Cloudflare Durable Objects are a novel approach to stateful compute based on Cloudflare Workers. They aim to locate both compute and state closest to end users.    This page tracks where new Durable Objects are created; for example, when you loaded this page from Half Moon Bay, a worker in San Jose, California, United States (SJC) created a durable object in Los Angeles, California, United States (LAX).    Currently, Durable Objects are available in 11.35% of Cloudflare PoPs.    To keep data fresh, this application is constantly creating/destroying new Durable Objects around the world. In the last hour, 394,046 Durable Objects have been created(and subsequently destroyed), FOR SCIENCE!    And a map of the world showing lots of dots." src="https://static.simonwillison.net/static/2024/where-durable-objects.jpg" /&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/kjx2vk/zero_latency_sqlite_storage_every"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/websockets"&gt;websockets&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-architecture"&gt;software-architecture&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/litestream"&gt;litestream&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kenton-varda"&gt;kenton-varda&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="sqlite"/><category term="websockets"/><category term="software-architecture"/><category term="cloudflare"/><category term="litestream"/><category term="kenton-varda"/></entry><entry><title>Bringing Python to Workers using Pyodide and WebAssembly</title><link href="https://simonwillison.net/2024/Apr/2/cloudflare-python-workers/#atom-tag" rel="alternate"/><published>2024-04-02T16:09:57+00:00</published><updated>2024-04-02T16:09:57+00:00</updated><id>https://simonwillison.net/2024/Apr/2/cloudflare-python-workers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.cloudflare.com/python-workers"&gt;Bringing Python to Workers using Pyodide and WebAssembly&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cloudflare Workers is Cloudflare’s serverless hosting tool for deploying server-side functions to edge locations in their CDN.&lt;/p&gt;

&lt;p&gt;They just released Python support, accompanied by an extremely thorough technical explanation of how they got that to work. The details are fascinating.&lt;/p&gt;

&lt;p&gt;Workers runs on V8 isolates, and the new Python support was implemented using Pyodide (CPython compiled to WebAssembly) running inside V8.&lt;/p&gt;

&lt;p&gt;Getting this to work performantly and ergonomically took a huge amount of work.&lt;/p&gt;

&lt;p&gt;There are too many details in here to effectively summarize, but my favorite detail is this one:&lt;/p&gt;

&lt;p&gt;“We scan the Worker’s code for import statements, execute them, and then take a snapshot of the Worker’s WebAssembly linear memory. Effectively, we perform the expensive work of importing packages at deploy time, rather than at runtime.”

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/serverless"&gt;serverless&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="serverless"/><category term="cloudflare"/><category term="webassembly"/><category term="pyodide"/></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>Analytics: Hacker News v.s. a tweet from Elon Musk</title><link href="https://simonwillison.net/2023/Feb/17/analytics/#atom-tag" rel="alternate"/><published>2023-02-17T22:11:44+00:00</published><updated>2023-02-17T22:11:44+00:00</updated><id>https://simonwillison.net/2023/Feb/17/analytics/#atom-tag</id><summary type="html">
    &lt;p&gt;My post &lt;a href="https://simonwillison.net/2023/Feb/15/bing/"&gt;Bing: “I will not harm you unless you harm me first”&lt;/a&gt; really took off.&lt;/p&gt;
&lt;p&gt;It sat &lt;a href="https://news.ycombinator.com/item?id=34804874"&gt;at the top of Hacker News&lt;/a&gt; for a full day, and is currently &lt;a href="https://hn.algolia.com/"&gt;the 18th most popular post&lt;/a&gt; of all time on that site.&lt;/p&gt;
&lt;p&gt;And then this happened:&lt;/p&gt;

&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Might need a bit more polish …&lt;a href="https://t.co/rGYCxoBVeA"&gt;https://t.co/rGYCxoBVeA&lt;/a&gt;&lt;/p&gt;- Elon Musk (@elonmusk) &lt;a href="https://twitter.com/elonmusk/status/1625936009841213440?ref_src=twsrc%5Etfw"&gt;February 15, 2023&lt;/a&gt;&lt;/blockquote&gt;

&lt;p&gt;Given &lt;a href="https://www.theverge.com/2023/2/14/23600358/elon-musk-tweets-algorithm-changes-twitter"&gt;recent changes&lt;/a&gt; made to the Twitter algorithm, a &lt;em&gt;lot&lt;/em&gt; of people saw that. Twitter currently reports 30.4M views of that tweet.&lt;/p&gt;
&lt;p&gt;A bunch of people asked me how much of that converted into page views. So let's dive in!&lt;/p&gt;
&lt;h4&gt;Headline figures&lt;/h4&gt;
&lt;p&gt;Here's my Plausible dashboard for that post over the past few days:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/plausible-bing.jpg" alt="simonwillison.net on Plausible, filtered for /2023/Feb/15/bing/ - there's a huge spike in traffic starting on the 16th of Feb. 959k unique visitors, 1.1M page views, 90% bounce rate, 42m43s time on page. Top sources of traffic are Twitter at 721k, Direct / None at 132k, Hacker News at 49.5k, Facebook at 13.4k, Reddit at 8.3x, Google at 7.8k, tldrnewsletter at 6k and LinkedIn at 5.4k" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Overall numbers: 959k unique visitors, 1.1M page views.&lt;/p&gt;
&lt;p&gt;Top sources of traffic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Twitter: 721k&lt;/li&gt;
&lt;li&gt;Direct / None: 132k (this includes traffic from Mastodon)&lt;/li&gt;
&lt;li&gt;Hacker News: 49.5k&lt;/li&gt;
&lt;li&gt;Facebook: 13.4k&lt;/li&gt;
&lt;li&gt;Reddit: 8.3k&lt;/li&gt;
&lt;li&gt;Google: 7.8k&lt;/li&gt;
&lt;li&gt;tldrnewsletter: 6k&lt;/li&gt;
&lt;li&gt;LinkedIn: 5.4k&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If we assume the vast majority of the Twitter traffic was from Elon (which seems reasonable) that's 30.4M / 721k = roughly a 2.37% click through rate.&lt;/p&gt;
&lt;p&gt;Notable that sticking at the top of Hacker News for a day really does drive an enormous amount of traffic - 18% of the traffic you get from the second most followed account on Twitter (looks like &lt;a href="https://twitter.com/barackobama"&gt;Barack Obama&lt;/a&gt; is still number one).&lt;/p&gt;
&lt;h4&gt;More detailed analytics via Plausible and Cloudflare&lt;/h4&gt;
&lt;p&gt;I mainly use &lt;a href="https://plausible.io/"&gt;Plausible&lt;/a&gt; for my site's analytics. I really like them: they're privacy-focused, open source (though I use their hosted version) and show me exactly the subset of data I want to see. Most importantly, they don't set cookies.&lt;/p&gt;
&lt;p&gt;My site also runs behind &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt;, which also provides analytics. I don't pay for the upgraded analytics, but it turns out you can still get some pretty detailed numbers out of them - especially if you're willing to dig around in the browser DevTools.&lt;/p&gt;
&lt;p&gt;Plausible offers an "export" button, so I used that... and got a zip file with a bunch of CSVs in it. &lt;a href="https://github.com/simonw/i-will-not-harm-you-unless-you-harm-me-first/tree/main/plausible-csvs"&gt;Here they are&lt;/a&gt; in a GitHub repo.&lt;/p&gt;
&lt;p&gt;Cloudflare - at least for the free tier - doesn't have a detailed export. But... under the hood the Cloudflare web application &lt;a href="https://developers.cloudflare.com/analytics/graphql-api/"&gt;uses their GraphQL API&lt;/a&gt; to retrieve stats for display, and with a bit of digging you can get numbers out that way.&lt;/p&gt;
&lt;p&gt;I extracted &lt;a href="https://github.com/simonw/i-will-not-harm-you-unless-you-harm-me-first/blob/main/cloudflare.json"&gt;this 3.2MB JSON file&lt;/a&gt; using the Cloudflare API.&lt;/p&gt;
&lt;h4&gt;Loading it into Datasette&lt;/h4&gt;
&lt;p&gt;I wrote &lt;a href="https://github.com/simonw/i-will-not-harm-you-unless-you-harm-me-first/blob/main/build-dbs.sh"&gt;this script&lt;/a&gt; to load the data I had extracted into SQLite database files, and then deployed them to Vercel using &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can explore the result here: &lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/"&gt;https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/visitors?_sort=rowid&amp;amp;date__gte=2023-02-15#g.mark=bar&amp;amp;g.x_column=date&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=pageviews&amp;amp;g.y_type=quantitative"&gt;Here's page views according to Plausible&lt;/a&gt; over the time period in question:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/datasette-plausible-pageviews.jpg" alt="Chart in Datasette showing page views per hour according to Plausible - a big jump up to around 185,000 at 11am on the 15th" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It looks to me like the timezone for that data is Pacific Time.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/cloudflare/timeslots#g.mark=bar&amp;amp;g.x_column=timeslot&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=pageViews&amp;amp;g.y_type=quantitative"&gt;This page&lt;/a&gt; shows page views count according to Cloudflare, by hour.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/datasette-cloudflare-pageview.jpg" alt="Datasette interafce showing a chart plotted using the datasette-vega plugin - the chart shows pageviews against time spiking up to just over 200,000 at 7pm UTC on 15th Feb, the time of the Elon tweet" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This data is in UTC, where 7pm UTC corresponds to 11am Pacific.&lt;/p&gt;
&lt;p&gt;These numbers should differ, because Plausible uses JavaScript to track analytics while Cloudflare is server-side, plus Plausible is filtered to just hits to the specific page while Cloudflare is showing all hits to any page on my site.&lt;/p&gt;
&lt;p&gt;There are plenty more ways to slice and dice the data in Datasette:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/visitors?_sort=rowid&amp;amp;date__gte=2023-02-15#g.mark=bar&amp;amp;g.x_column=date&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=visitors&amp;amp;g.y_type=quantitative"&gt;Unique visitors over time according to Plausible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/cloudflare/timeslots#g.mark=bar&amp;amp;g.x_column=timeslot&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=uniques&amp;amp;g.y_type=quantitative"&gt;Uniques over time according to Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/sources#g.mark=bar&amp;amp;g.x_column=name&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=visitors&amp;amp;g.y_type=quantitative"&gt;Full data for those traffic sources from Plausible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/devices"&gt;Plausible device breakdown&lt;/a&gt; - 778,678 mobile, 101,216 desktop, 47,781 laptop (not sure how it distinguishes between desktop and laptop though), 16,967 tablet.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/cloudflare?sql=select+timeslot%2C+requests%2C+cachedRequests%2C+100.0+*+cachedRequests+%2F+requests+as+pctCached+from+timeslots+order+by+timeslot+limit+101#g.mark=line&amp;amp;g.x_column=timeslot&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=pctCached&amp;amp;g.y_type=quantitative"&gt;Percentage of cached requests over time according to Cloudflare&lt;/a&gt; using a custom SQL query - this was around 40% before the Elon tweet, then jumped up to over 90% and stayed there, thankfully!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I've long been a fan of full-page HTTP caching as protection against surprise traffic events - it's a pattern I've implemented in the past using Varnish and Fastly, and I've been using it on my blog via Cloudflare for several years.&lt;/p&gt;
&lt;p&gt;It definitely paid off this time!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/analytics"&gt;analytics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/bing"&gt;bing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&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;/p&gt;
    

</summary><category term="analytics"/><category term="bing"/><category term="hacker-news"/><category term="twitter"/><category term="datasette"/><category term="cloudflare"/></entry><entry><title>Wildebeest</title><link href="https://simonwillison.net/2023/Jan/23/wildebeest/#atom-tag" rel="alternate"/><published>2023-01-23T00:03:30+00:00</published><updated>2023-01-23T00:03:30+00:00</updated><id>https://simonwillison.net/2023/Jan/23/wildebeest/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/cloudflare/wildebeest"&gt;Wildebeest&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New project from Cloudflare, first quietly unveiled three weeks ago: “Wildebeest is an ActivityPub and Mastodon-compatible server”. It’s built using a flurry of Cloudflare-specific technology, including Workers, Pages and their SQLite-based D1 database.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://fedi.simonwillison.net/@simon/109735539970564454"&gt;@simon&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mastodon"&gt;mastodon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/activitypub"&gt;activitypub&lt;/a&gt;&lt;/p&gt;



</summary><category term="sqlite"/><category term="cloudflare"/><category term="mastodon"/><category term="activitypub"/></entry><entry><title>Stringing together several free tiers to host an application with zero cost using fly.io, Litestream and Cloudflare</title><link href="https://simonwillison.net/2022/Oct/7/fly-cloudflare/#atom-tag" rel="alternate"/><published>2022-10-07T17:47:34+00:00</published><updated>2022-10-07T17:47:34+00:00</updated><id>https://simonwillison.net/2022/Oct/7/fly-cloudflare/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.dahl.dev/posts/stringing-together-several-free-tiers-to-host-an-application-with-zero-cost/"&gt;Stringing together several free tiers to host an application with zero cost using fly.io, Litestream and Cloudflare&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Alexander Dahl provides a detailed description (and code) for his current preferred free hosting solution for small sites: SQLite (and a Go application) running on Fly’s free tier, with the database replicated up to Cloudflare’s R2 object storage (again on a free tier) by Litestream.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hosting"&gt;hosting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/litestream"&gt;litestream&lt;/a&gt;&lt;/p&gt;



</summary><category term="hosting"/><category term="sqlite"/><category term="cloudflare"/><category term="fly"/><category term="litestream"/></entry><entry><title>1.1.1.1/purge-cache</title><link href="https://simonwillison.net/2021/Dec/6/purge-cache/#atom-tag" rel="alternate"/><published>2021-12-06T23:15:08+00:00</published><updated>2021-12-06T23:15:08+00:00</updated><id>https://simonwillison.net/2021/Dec/6/purge-cache/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://1.1.1.1/purge-cache/"&gt;1.1.1.1/purge-cache&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cloudflare’s 1.1.1.1 DNS service has a tool that anyone can use to flush a specific DNS entry from their cache—could be useful for assisting rollouts of new DNS configurations.

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


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



</summary><category term="dns"/><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>Details of the Cloudflare outage on July 2, 2019</title><link href="https://simonwillison.net/2019/Jul/12/details-cloudflare-outage-july-2-2019/#atom-tag" rel="alternate"/><published>2019-07-12T17:36:25+00:00</published><updated>2019-07-12T17:36:25+00:00</updated><id>https://simonwillison.net/2019/Jul/12/details-cloudflare-outage-july-2-2019/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/"&gt;Details of the Cloudflare outage on July 2, 2019&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Best retrospective I’ve read in a long time. The outage was caused by a backtracking regex rule that was added to the Web Application Firewall project, which rolls out globally and skips most of Cloudflare’s regular graduar rollout process (delightfully animal themed, named DOG for the dogfooding PoP that their employees use, PIG for the Guinea Pig PoPs reserved for free customers, then Canary for the final step) so that they can deploy counter-measures to newly discovered vulnerabilities as quickly as possible—but the real value in the retro is that it provides an extremely deep insight into how Cloudflare organize, test and manage their changes. Really interesting stuff.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/operations"&gt;operations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/regular-expressions"&gt;regular-expressions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postmortem"&gt;postmortem&lt;/a&gt;&lt;/p&gt;



</summary><category term="operations"/><category term="regular-expressions"/><category term="cloudflare"/><category term="postmortem"/></entry><entry><title>The Now CDN</title><link href="https://simonwillison.net/2018/Jul/12/now-cdn/#atom-tag" rel="alternate"/><published>2018-07-12T03:34:06+00:00</published><updated>2018-07-12T03:34:06+00:00</updated><id>https://simonwillison.net/2018/Jul/12/now-cdn/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zeit.co/blog/now-cdn"&gt;The Now CDN&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Huge announcement from Zeit Now today: all .now.sh deployments are now served through the Cloudflare CDN, which means they benefit from 150 worldwide CDN locations that obey HTTP caching headers. This is particularly relevant for Datasette, since it serves far-future cache headers by default and uses Cloudflare-compatible HTTP/2 push hints to accelerate 302 redirects. This means that both the “datasette publish now” CLI command and the Datasette Publish web app will now result in Cloudflare-accelerated deployments.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/zeithq/status/1017058342945087489"&gt;@zeithq&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cdn"&gt;cdn&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&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;/p&gt;



</summary><category term="cdn"/><category term="performance"/><category term="zeit-now"/><category term="datasette"/><category term="cloudflare"/></entry><entry><title>Everyone can now run JavaScript on Cloudflare with Workers</title><link href="https://simonwillison.net/2018/Mar/13/javascript-on-cloudflare-with-workers/#atom-tag" rel="alternate"/><published>2018-03-13T16:36:53+00:00</published><updated>2018-03-13T16:36:53+00:00</updated><id>https://simonwillison.net/2018/Mar/13/javascript-on-cloudflare-with-workers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.cloudflare.com/cloudflare-workers-unleashed/amp/?__twitter_impression=true"&gt;Everyone can now run JavaScript on Cloudflare with Workers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is such a brilliant piece of software design: Cloudflare took the service workers spec and used it as the basis for their edge-executed JacaScript feature. This means you can run server-side JavaScript in hundreds of edge locations worldwide, applying custom dynamic logic (including additional async cached fetch() calls) with only around 1ms if additional overhead. The pricing model is a steal: $0.50 per million requests with a $5/month minimum.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cdn"&gt;cdn&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/serviceworkers"&gt;serviceworkers&lt;/a&gt;&lt;/p&gt;



</summary><category term="cdn"/><category term="javascript"/><category term="cloudflare"/><category term="serviceworkers"/></entry></feed>