<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: scaling</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/scaling.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-04-24T01:08:17+00:00</updated><author><name>Simon Willison</name></author><entry><title>Serving the For You feed</title><link href="https://simonwillison.net/2026/Apr/24/serving-the-for-you-feed/#atom-tag" rel="alternate"/><published>2026-04-24T01:08:17+00:00</published><updated>2026-04-24T01:08:17+00:00</updated><id>https://simonwillison.net/2026/Apr/24/serving-the-for-you-feed/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://atproto.com/blog/serving-the-for-you-feed"&gt;Serving the For You feed&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
One of Bluesky's most interesting features is that anyone can run their own &lt;a href="bluesky custom feed"&gt;custom "feed" implementation&lt;/a&gt; and make it available to other users - effectively enabling custom algorithms that can use any mechanism they like to recommend posts.&lt;/p&gt;
&lt;p&gt;spacecowboy runs the &lt;a href="https://bsky.app/profile/did:plc:3guzzweuqraryl3rdkimjamk/feed/for-you"&gt;For You Feed&lt;/a&gt;, used by around 72,000 people. This guest post on the AT Protocol blog explains how it works.&lt;/p&gt;
&lt;p&gt;The architecture is &lt;em&gt;fascinating&lt;/em&gt;. The feed is served by a single Go process using SQLite on a "gaming" PC in spacecowboy's living room - 16 cores, 96GB of RAM and 4TB of attached NVMe storage.&lt;/p&gt;
&lt;p&gt;Recommendations are based on likes: what else are the people who like the same things as you liking on the platform?&lt;/p&gt;
&lt;p&gt;That Go server consumes the Bluesky firehose and stores the relevant details in SQLite, keeping the last 90 days of relevant data, which currently uses around 419GB of SQLite storage.&lt;/p&gt;
&lt;p&gt;Public internet traffic is handled by a $7/month VPS on OVH, which talks to the living room server via Tailscale.&lt;/p&gt;
&lt;p&gt;Total cost is now $30/month: $20 in electricity, $7 in VPS and $3 for the two domain names. spacecowboy estimates that the existing system could handle all ~1 million daily active Bluesky users if they were to switch to the cheapest algorithm they have found to work.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &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/software-architecture"&gt;software-architecture&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tailscale"&gt;tailscale&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/bluesky"&gt;bluesky&lt;/a&gt;&lt;/p&gt;



</summary><category term="go"/><category term="scaling"/><category term="sqlite"/><category term="software-architecture"/><category term="tailscale"/><category term="bluesky"/></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>The case against pgvector</title><link href="https://simonwillison.net/2025/Nov/3/the-case-against-pgvector/#atom-tag" rel="alternate"/><published>2025-11-03T20:26:10+00:00</published><updated>2025-11-03T20:26:10+00:00</updated><id>https://simonwillison.net/2025/Nov/3/the-case-against-pgvector/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://alex-jacobs.com/posts/the-case-against-pgvector/"&gt;The case against pgvector&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I wasn't keen on the title of this piece but the content is great: Alex Jacobs talks through lessons learned trying to run the popular pgvector PostgreSQL vector indexing extension at scale, in particular the challenges involved in maintaining a large index with close-to-realtime updates using the IVFFlat or HNSW index types.&lt;/p&gt;
&lt;p&gt;The section on pre-v.s.-post filtering is particularly useful:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Okay but let's say you solve your index and insert problems. Now you have a document search system with millions of vectors. Documents have metadata---maybe they're marked as &lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;published&lt;/code&gt;, or &lt;code&gt;archived&lt;/code&gt;. A user searches for something, and you only want to return published documents.&lt;/p&gt;
&lt;p&gt;[...] should Postgres filter on status first (pre-filter) or do the vector search first and then filter (post-filter)?&lt;/p&gt;
&lt;p&gt;This seems like an implementation detail. It’s not. It’s the difference between queries that take 50ms and queries that take 5 seconds. It’s also the difference between returning the most relevant results and… not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href="https://news.ycombinator.com/item?id=45798479"&gt;Hacker News thread&lt;/a&gt; for this article attracted a robust discussion, including some fascinating comments by Discourse developer Rafael dos Santos Silva (xfalcox) about how they are using pgvector at scale:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We [run pgvector in production] at Discourse, in thousands of databases, and it's leveraged in most of the billions of page views we serve. [...]&lt;/p&gt;
&lt;p&gt;Also worth mentioning that we use quantization extensively:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;halfvec (16bit float) for storage - bit (binary vectors) for indexes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which makes the storage cost and on-going performance good enough that we could enable this in all our hosting. [...]&lt;/p&gt;
&lt;p&gt;In Discourse embeddings power:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Related Topics, a list of topics to read next, which uses embeddings of the current topic as the key to search for similar ones&lt;/li&gt;
&lt;li&gt;Suggesting tags and categories when composing a new topic&lt;/li&gt;
&lt;li&gt;Augmented search&lt;/li&gt;
&lt;li&gt;RAG for uploaded files&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vector-search"&gt;vector-search&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;&lt;/p&gt;



</summary><category term="postgresql"/><category term="scaling"/><category term="vector-search"/><category term="embeddings"/></entry><entry><title>Quoting AWS</title><link href="https://simonwillison.net/2025/Oct/23/aws-postmortem/#atom-tag" rel="alternate"/><published>2025-10-23T04:49:59+00:00</published><updated>2025-10-23T04:49:59+00:00</updated><id>https://simonwillison.net/2025/Oct/23/aws-postmortem/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://aws.amazon.com/message/101925/"&gt;&lt;p&gt;For resiliency, the DNS Enactor operates redundantly and fully independently in three different Availability Zones (AZs). [...] When the second Enactor (applying the newest plan) completed its endpoint updates, it then invoked the plan clean-up process, which identifies plans that are significantly older than the one it just applied and deletes them. At the same time that this clean-up process was invoked, the first Enactor (which had been unusually delayed) applied its much older plan to the regional DDB endpoint, overwriting the newer plan. [...] The second Enactor's clean-up process then deleted this older plan because it was many generations older than the plan it had just applied. As this plan was deleted, all IP addresses for the regional endpoint were immediately removed.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://aws.amazon.com/message/101925/"&gt;AWS&lt;/a&gt;, Amazon DynamoDB Service Disruption in Northern Virginia (US-EAST-1) Region (14.5 hours long!)&lt;/p&gt;

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



</summary><category term="aws"/><category term="dns"/><category term="scaling"/><category term="postmortem"/></entry><entry><title>Quoting Kumar Aditya</title><link href="https://simonwillison.net/2025/Sep/11/kumar-aditya/#atom-tag" rel="alternate"/><published>2025-09-11T03:07:16+00:00</published><updated>2025-09-11T03:07:16+00:00</updated><id>https://simonwillison.net/2025/Sep/11/kumar-aditya/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://labs.quansight.org/blog/scaling-asyncio-on-free-threaded-python"&gt;&lt;p&gt;In Python 3.14, I have implemented several changes to fix thread safety of &lt;code&gt;asyncio&lt;/code&gt; and enable it to scale effectively on the free-threaded build of CPython. It is now implemented using lock-free data structures and per-thread state, allowing for highly efficient task management and execution across multiple threads. In the general case of multiple event loops running in parallel, there is no lock contention and performance scales linearly with the number of threads. [...]&lt;/p&gt;
&lt;p&gt;For a deeper dive into the implementation, check out the &lt;a href="https://github.com/python/cpython/blob/main/InternalDocs/asyncio.md#python-314-implementation"&gt;internal docs for asyncio&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://labs.quansight.org/blog/scaling-asyncio-on-free-threaded-python"&gt;Kumar Aditya&lt;/a&gt;, Scaling asyncio on Free-Threaded Python&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gil"&gt;gil&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/threads"&gt;threads&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="gil"/><category term="python"/><category term="scaling"/><category term="threads"/></entry><entry><title>Announcing PlanetScale for Postgres</title><link href="https://simonwillison.net/2025/Jul/1/planetscale-for-postgres/#atom-tag" rel="alternate"/><published>2025-07-01T18:16:12+00:00</published><updated>2025-07-01T18:16:12+00:00</updated><id>https://simonwillison.net/2025/Jul/1/planetscale-for-postgres/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://planetscale.com/blog/planetscale-for-postgres#vitess-for-postgres"&gt;Announcing PlanetScale for Postgres&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
PlanetScale formed in 2018 to build a commercial offering on top of the Vitess MySQL sharding open source project, which was originally released by YouTube in 2012. The PlanetScale founders were the co-creators and maintainers of Vitess.&lt;/p&gt;
&lt;p&gt;Today PlanetScale are announcing a private preview of their new horizontally sharded PostgreSQL solution, due to "overwhelming" demand.&lt;/p&gt;
&lt;p&gt;Notably, it doesn't use Vitess under the hood:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Vitess is one of PlanetScale’s greatest strengths [...] We have made explicit sharding accessible to hundreds of thousands of users and it is time to bring this power to Postgres. We will not however be using Vitess to do this.&lt;/p&gt;
&lt;p&gt;Vitess’ achievements are enabled by leveraging MySQL’s strengths and engineering around its weaknesses. To achieve Vitess’ power for Postgres we are architecting from first principles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Meanwhile, on June 10th Supabase announced that they had &lt;a href="https://supabase.com/blog/multigres-vitess-for-postgres"&gt;hired Vitess co-creator  Sugu Sougoumarane&lt;/a&gt; to help them build "Multigres: Vitess for Postgres". Sugu said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For some time, I've been considering a Vitess adaptation for Postgres, and this feeling had been gradually intensifying. The recent explosion in the popularity of Postgres has fueled this into a full-blown obsession. [...]&lt;/p&gt;
&lt;p&gt;The project to address this problem must begin now, and I'm convinced that Vitess provides the most promising foundation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I remember when MySQL was an order of magnitude more popular than PostgreSQL, and Heroku's decision to only offer PostgreSQL back in 2007 was a surprising move. The vibes have certainly shifted.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mysql"&gt;mysql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sharding"&gt;sharding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vitess"&gt;vitess&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="mysql"/><category term="postgresql"/><category term="scaling"/><category term="sharding"/><category term="vitess"/></entry><entry><title>GitHub Issues search now supports nested queries and boolean operators: Here’s how we (re)built it</title><link href="https://simonwillison.net/2025/May/26/github-issues-search/#atom-tag" rel="alternate"/><published>2025-05-26T07:23:17+00:00</published><updated>2025-05-26T07:23:17+00:00</updated><id>https://simonwillison.net/2025/May/26/github-issues-search/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.blog/developer-skills/application-development/github-issues-search-now-supports-nested-queries-and-boolean-operators-heres-how-we-rebuilt-it/"&gt;GitHub Issues search now supports nested queries and boolean operators: Here’s how we (re)built it&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
GitHub Issues got a significant search upgrade &lt;a href="https://simonwillison.net/2025/Jan/16/evolving-github-issues/"&gt;back in January&lt;/a&gt;. Deborah Digges provides some behind the scene details about how it works and how they rolled it out.&lt;/p&gt;
&lt;p&gt;The signature new feature is complex boolean logic: you can now search for things like &lt;code&gt;is:issue state:open author:rileybroughten (type:Bug OR type:Epic)&lt;/code&gt;, up to five levels of nesting deep.&lt;/p&gt;
&lt;p&gt;Queries are parsed into an AST using the Ruby &lt;a href="https://github.com/kschiess/parslet"&gt;parslet&lt;/a&gt; PEG grammar library. The AST is then compiled into a nested Elasticsearch &lt;code&gt;bool&lt;/code&gt; JSON query.&lt;/p&gt;
&lt;p&gt;GitHub Issues search deals with around 2,000 queries a second so robust testing is extremely important! The team rolled it out invisibly to 1% of live traffic, running the new implementation via a queue and competing the number of results returned to try and spot any degradations compared to the old production code.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/elasticsearch"&gt;elasticsearch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ops"&gt;ops&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/parsing"&gt;parsing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/search"&gt;search&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-issues"&gt;github-issues&lt;/a&gt;&lt;/p&gt;



</summary><category term="elasticsearch"/><category term="github"/><category term="ops"/><category term="parsing"/><category term="ruby"/><category term="scaling"/><category term="search"/><category term="github-issues"/></entry><entry><title>Building, launching, and scaling ChatGPT Images</title><link href="https://simonwillison.net/2025/May/13/launching-chatgpt-images/#atom-tag" rel="alternate"/><published>2025-05-13T23:52:22+00:00</published><updated>2025-05-13T23:52:22+00:00</updated><id>https://simonwillison.net/2025/May/13/launching-chatgpt-images/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://newsletter.pragmaticengineer.com/p/chatgpt-images"&gt;Building, launching, and scaling ChatGPT Images&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Gergely Orosz landed a fantastic deep dive interview with OpenAI's Sulman Choudhry (head of engineering, ChatGPT) and Srinivas Narayanan (VP of engineering, OpenAI) to talk about the launch back in March of ChatGPT images - their new image generation mode built on top of multi-modal GPT-4o.&lt;/p&gt;
&lt;p&gt;The feature kept on having new viral spikes, including one that added one million new users in a single hour. They signed up 100 million new users in the first week after the feature's launch.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When this vertical growth spike started, most of our engineering teams didn't believe it. They assumed there must be something wrong with the metrics.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Under the hood the infrastructure is mostly Python and &lt;a href="https://github.com/fastapi/fastapi"&gt;FastAPI&lt;/a&gt;! I hope they're sponsoring those projects (and &lt;a href="https://github.com/encode/starlette"&gt;Starlette&lt;/a&gt;, which is used by FastAPI under the hood.)&lt;/p&gt;
&lt;p&gt;They're also using some C, and &lt;a href="https://temporal.io/"&gt;Temporal&lt;/a&gt; as a workflow engine. They addressed the early scaling challenge by adding an asynchronous queue to defer the load for their free users (resulting in longer generation times) at peak demand.&lt;/p&gt;
&lt;p&gt;There are plenty more details tucked away behind the firewall, including an exclusive I've not been able to find anywhere else: OpenAI's core engineering principles.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ship relentlessly&lt;/strong&gt; - move quickly and continuously improve, without waiting for perfect conditions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Own the outcome&lt;/strong&gt; - take full responsibility for products, end-to-end&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follow through&lt;/strong&gt; - finish what is started and ensure the work lands fully&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I tried getting o4-mini-high to track down a copy of those principles online and was delighted to see it either leak or hallucinate the URL to OpenAI's internal engineering handbook!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Thought for 1m 48s:  Here are the two places where OpenAl's “Ship  relentlessly — Own the outcome — Follow through”  checklist shows up as its core engineering  principles:  1. OpenAl Careers =&amp;gt; Operating Principles  On the Careers page under &amp;quot;Operating Principles,”  OpenAl codifies how every team—and especially  engineering—works together. While the page itself  doesn’t list the bullets verbatim, it's the umbrella  under which these three behavioural norms sit (Ship  relentlessly, Own the outcome, Follow through).  2. OpenAl Internal Engineering Handbook  These three tenets are drawn straight from OpenAl's  internal Engineering Handbook (employee-only), at: https://handbook.openai.com/engineering/principles  (This handbook page isn't publicly crawlable, but it's  the definitive source for how OpenAl's engineers  actually organize their day-to-day work.)" src="https://static.simonwillison.net/static/2025/openai-handbook.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Gergely has a whole series of posts like this called &lt;a href="https://newsletter.pragmaticengineer.com/t/real-world-engineering-challenges"&gt;Real World Engineering Challenges&lt;/a&gt;, including another one &lt;a href="https://newsletter.pragmaticengineer.com/p/scaling-chatgpt"&gt;on ChatGPT a year ago&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/GergelyOrosz/status/1922388794377961692"&gt;@GergelyOrosz&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/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&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/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/gergely-orosz"&gt;gergely-orosz&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="scaling"/><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="gergely-orosz"/><category term="starlette"/></entry><entry><title>Merklemap runs a 16TB PostgreSQL</title><link href="https://simonwillison.net/2025/Mar/14/merklemap-runs-a-16tb-postgresql/#atom-tag" rel="alternate"/><published>2025-03-14T20:13:41+00:00</published><updated>2025-03-14T20:13:41+00:00</updated><id>https://simonwillison.net/2025/Mar/14/merklemap-runs-a-16tb-postgresql/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=43364668#43365833"&gt;Merklemap runs a 16TB PostgreSQL&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Interesting thread on Hacker News where Pierre Barre describes the database architecture behind &lt;a href="https://www.merklemap.com/"&gt;Merklemap&lt;/a&gt;, a certificate transparency search engine.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I run a 100 billion+ rows Postgres database [0], that is around 16TB, it's pretty painless!&lt;/p&gt;
&lt;p&gt;There are a few tricks that make it run well (PostgreSQL compiled with a non-standard block size, ZFS, careful VACUUM planning). But nothing too out of the ordinary.&lt;/p&gt;
&lt;p&gt;ATM, I insert about 150,000 rows a second, run 40,000 transactions a second, and read 4 million rows a second.&lt;/p&gt;
&lt;p&gt;[...]&lt;/p&gt;
&lt;p&gt;It's self-hosted on bare metal, with standby replication, normal settings, nothing "weird" there.&lt;/p&gt;
&lt;p&gt;6 NVMe drives in raidz-1, 1024GB of memory, a 96 core AMD EPYC cpu.&lt;/p&gt;
&lt;p&gt;[...]&lt;/p&gt;
&lt;p&gt;About 28K euros of hardware per replica [one-time cost] IIRC + [ongoing] colo costs.&lt;/p&gt;
&lt;/blockquote&gt;


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



</summary><category term="postgresql"/><category term="scaling"/></entry><entry><title>Quoting Jeremy Edberg</title><link href="https://simonwillison.net/2024/Dec/24/jeremy-edberg/#atom-tag" rel="alternate"/><published>2024-12-24T07:13:01+00:00</published><updated>2024-12-24T07:13:01+00:00</updated><id>https://simonwillison.net/2024/Dec/24/jeremy-edberg/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=42486610#42492484"&gt;&lt;p&gt;[On Reddit] we had to look up every single comment on the page to see if you had voted on it [...]&lt;/p&gt;
&lt;p&gt;But with a bloom filter, we could very quickly look up all the comments and get back a list of all the ones you voted on (with a couple of false positives in there). Then we could go to the cache and see if your actual vote was there (and if it was an upvote or a downvote). It was only after a failed cache hit did we have to actually go to the database.&lt;/p&gt;
&lt;p&gt;But that bloom filter saved us from doing sometimes 1000s of cache lookups.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=42486610#42492484"&gt;Jeremy Edberg&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/bloom-filters"&gt;bloom-filters&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/reddit"&gt;reddit&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;&lt;/p&gt;



</summary><category term="bloom-filters"/><category term="reddit"/><category term="scaling"/></entry><entry><title>DSQL Vignette: Reads and Compute</title><link href="https://simonwillison.net/2024/Dec/6/dsql-vignette-reads-and-compute/#atom-tag" rel="alternate"/><published>2024-12-06T17:12:10+00:00</published><updated>2024-12-06T17:12:10+00:00</updated><id>https://simonwillison.net/2024/Dec/6/dsql-vignette-reads-and-compute/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://brooker.co.za/blog/2024/12/04/inside-dsql.html"&gt;DSQL Vignette: Reads and Compute&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Marc Brooker is one of the engineers behind AWS's new &lt;a href="https://simonwillison.net/2024/Dec/3/amazon-aurora-dsql/"&gt;Aurora DSQL&lt;/a&gt; horizontally scalable database. Here he shares all sorts of interesting details about how it works under the hood.&lt;/p&gt;
&lt;p&gt;The system is built around the principle of separating storage from compute: storage uses S3, while compute runs in Firecracker:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Each transaction inside DSQL runs in a customized Postgres engine inside a Firecracker MicroVM, dedicated to your database. When you connect to DSQL, we make sure there are enough of these MicroVMs to serve your load, and scale up dynamically if needed. We add MicroVMs in the AZs and regions your connections are coming from, keeping your SQL query processor engine as close to your client as possible to optimize for latency.&lt;/p&gt;
&lt;p&gt;We opted to use PostgreSQL here because of its pedigree, modularity, extensibility, and performance. We’re not using any of the storage or transaction processing parts of PostgreSQL, but are using the SQL engine, an adapted version of the planner and optimizer, and the client protocol implementation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The system then provides strong repeatable-read transaction isolation using MVCC and EC2's high precision clocks, enabling reads "as of time X" including against nearby read replicas.&lt;/p&gt;
&lt;p&gt;The storage layer supports index scans, which means the compute layer can push down some operations allowing it to load a subset of the rows it needs, reducing round-trips that are affected by speed-of-light latency.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The overall approach here is &lt;em&gt;disaggregation&lt;/em&gt;: we’ve taken each of the critical components of an OLTP database and made it a dedicated service. Each of those services is independently horizontally scalable, most of them are shared-nothing, and each can make the design choices that is most optimal in its domain.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aws"&gt;aws&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ec2"&gt;ec2&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&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/firecracker"&gt;firecracker&lt;/a&gt;&lt;/p&gt;



</summary><category term="aws"/><category term="databases"/><category term="ec2"/><category term="postgresql"/><category term="s3"/><category term="scaling"/><category term="software-architecture"/><category term="firecracker"/></entry><entry><title>Quoting Javi Santana</title><link href="https://simonwillison.net/2024/Dec/1/javi-santana/#atom-tag" rel="alternate"/><published>2024-12-01T05:02:42+00:00</published><updated>2024-12-01T05:02:42+00:00</updated><id>https://simonwillison.net/2024/Dec/1/javi-santana/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://javisantana.com/2024/11/30/learnings-after-4-years-data-eng.html"&gt;&lt;p&gt;Most people don’t have an intuition about what current hardware can and can’t do. There is a simple math that can help you with that: “you can process about 500MB in one second on a single machine”. I know it’s not a universal truth and there are a lot of details that can change that but believe me, this estimation is a pretty good tool to have under your belt.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://javisantana.com/2024/11/30/learnings-after-4-years-data-eng.html"&gt;Javi Santana&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/big-data"&gt;big-data&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="big-data"/></entry><entry><title>Amazon S3 adds new functionality for conditional writes</title><link href="https://simonwillison.net/2024/Nov/26/s3-conditional-writes/#atom-tag" rel="alternate"/><published>2024-11-26T01:14:29+00:00</published><updated>2024-11-26T01:14:29+00:00</updated><id>https://simonwillison.net/2024/Nov/26/s3-conditional-writes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-s3-functionality-conditional-writes/"&gt;Amazon S3 adds new functionality for conditional writes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Amazon S3 can now perform conditional writes that evaluate if an object is unmodified before updating it. This helps you coordinate simultaneous writes to the same object and prevents multiple concurrent writers from unintentionally overwriting the object without knowing the state of its content. You can use this capability by providing the ETag of an object [...]&lt;/p&gt;
&lt;p&gt;This new conditional header can help improve the efficiency of your large-scale analytics, distributed machine learning, and other highly parallelized workloads by reliably offloading compare and swap operations to S3.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(Both &lt;a href="https://learn.microsoft.com/en-us/rest/api/storageservices/specifying-conditional-headers-for-blob-service-operations#Subheading1"&gt;Azure Blob Storage&lt;/a&gt; and &lt;a href="https://cloud.google.com/storage/docs/request-preconditions#precondition_criteria"&gt;Google Cloud&lt;/a&gt; have this feature already.)&lt;/p&gt;
&lt;p&gt;When AWS added conditional write support just for if an object with that key exists or not back in August I &lt;a href="https://simonwillison.net/2024/Aug/30/leader-election-with-s3-conditional-writes/"&gt;wrote about&lt;/a&gt; Gunnar Morling's trick for &lt;a href="https://www.morling.dev/blog/leader-election-with-s3-conditional-writes/"&gt;Leader Election With S3 Conditional Writes&lt;/a&gt;. This new capability opens up a whole set of new patterns for implementing distributed locking systems along those lines.&lt;/p&gt;
&lt;p&gt;Here's a useful illustrative example &lt;a href="https://news.ycombinator.com/item?id=42240678#42241577"&gt;by lxgr on Hacker News&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As a (horribly inefficient, in case of non-trivial write contention) toy example, you could use S3 as a lock-free concurrent SQLite storage backend: Reads work as expected by fetching the entire database and satisfying the operation locally; writes work like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Download the current database copy&lt;/li&gt;
&lt;li&gt;Perform your write locally&lt;/li&gt;
&lt;li&gt;Upload it back using "Put-If-Match" and the pre-edit copy as the matched object.&lt;/li&gt;
&lt;li&gt;If you get success, consider the transaction successful.&lt;/li&gt;
&lt;li&gt;If you get failure, go back to step 1 and try again.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;AWS also just added the ability to &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-s3-enforcement-conditional-write-operations-general-purpose-buckets/"&gt;enforce conditional writes in bucket policies&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To enforce conditional write operations, you can now use s3:if-none-match or s3:if-match condition keys to write a bucket policy that mandates the use of HTTP if-none-match or HTTP if-match conditional headers in S3 PutObject and CompleteMultipartUpload API requests. With this bucket policy in place, any attempt to write an object to your bucket without the required conditional header will be rejected.&lt;/p&gt;
&lt;/blockquote&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aws"&gt;aws&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-architecture"&gt;software-architecture&lt;/a&gt;&lt;/p&gt;



</summary><category term="aws"/><category term="s3"/><category term="scaling"/><category term="software-architecture"/></entry><entry><title>Amazon S3 Express One Zone now supports the ability to append data to an object</title><link href="https://simonwillison.net/2024/Nov/22/amazon-s3-append-data/#atom-tag" rel="alternate"/><published>2024-11-22T04:39:35+00:00</published><updated>2024-11-22T04:39:35+00:00</updated><id>https://simonwillison.net/2024/Nov/22/amazon-s3-append-data/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-s3-express-one-zone-append-data-object/"&gt;Amazon S3 Express One Zone now supports the ability to append data to an object&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is a first for Amazon S3: it is now possible to append data to an existing object in a bucket, where previously the only supported operation was to atomically replace the object with an updated version.&lt;/p&gt;
&lt;p&gt;This is only available for S3 Express One Zone, a bucket class introduced &lt;a href="https://aws.amazon.com/blogs/aws/new-amazon-s3-express-one-zone-high-performance-storage-class/"&gt;a year ago&lt;/a&gt; which provides storage in just a single availability zone, providing significantly lower latency at the cost of reduced redundancy and a much higher price (16c/GB/month compared to 2.3c for S3 standard tier).&lt;/p&gt;
&lt;p&gt;The fact that appends have never been supported for multi-availability zone S3 provides an interesting clue as to the underlying architecture. Guaranteeing that every copy of an object has received and applied an append is significantly harder than doing a distributed atomic swap to a new version.&lt;/p&gt;
&lt;p&gt;More details from &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-buckets-objects-append.html"&gt;the documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There is no minimum size requirement for the data you can append to an object. However, the maximum size of the data that you can append to an object in a single request is 5GB. This is the same limit as the largest request size when uploading data using any Amazon S3 API.&lt;/p&gt;
&lt;p&gt;With each successful append operation, you create a part of the object and each object can have up to 10,000 parts. This means you can append data to an object up to 10,000 times. If an object is created using S3 multipart upload, each uploaded part is counted towards the total maximum of 10,000 parts. For example, you can append up to 9,000 times to an object created by multipart upload comprising of 1,000 parts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That 10,000 limit means this won't quite work for constantly appending to a log file in a bucket.&lt;/p&gt;
&lt;p&gt;Presumably it will be possible to "tail" an object that is receiving appended updates using the HTTP Range header.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aws"&gt;aws&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&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/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;



</summary><category term="aws"/><category term="s3"/><category term="scaling"/><category term="software-architecture"/><category term="http-range-requests"/></entry><entry><title>Using Rust in non-Rust servers to improve performance</title><link href="https://simonwillison.net/2024/Oct/23/using-rust-in-non-rust-servers/#atom-tag" rel="alternate"/><published>2024-10-23T15:45:42+00:00</published><updated>2024-10-23T15:45:42+00:00</updated><id>https://simonwillison.net/2024/Oct/23/using-rust-in-non-rust-servers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/pretzelhammer/rust-blog/blob/master/posts/rust-in-non-rust-servers.md"&gt;Using Rust in non-Rust servers to improve performance&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Deep dive into different strategies for optimizing part of a web server application - in this case written in Node.js, but the same strategies should work for Python as well - by integrating with Rust in different ways.&lt;/p&gt;
&lt;p&gt;The example app renders QR codes, initially using the pure JavaScript &lt;a href="https://www.npmjs.com/package/qrcode"&gt;qrcode&lt;/a&gt; package. That ran at 1,464 req/sec, but switching it to calling a tiny Rust CLI wrapper around the &lt;a href="https://crates.io/crates/qrcode"&gt;qrcode crate&lt;/a&gt; using Node.js &lt;code&gt;spawn()&lt;/code&gt; increased that to 2,572 req/sec.&lt;/p&gt;
&lt;p&gt;This is yet another reminder to me that I need to get over my &lt;code&gt;cgi-bin&lt;/code&gt; era bias that says that shelling out to another process during a web request is a bad idea. It turns out modern computers can quite happily spawn and terminate 2,500+ processes a second!&lt;/p&gt;
&lt;p&gt;The article optimizes further first through a Rust library compiled to WebAssembly (2,978 req/sec) and then through a Rust function exposed to Node.js as a native library (5,490 req/sec), then finishes with a full Rust rewrite of the server that replaces Node.js entirely, running at 7,212 req/sec.&lt;/p&gt;
&lt;p&gt;Full source code to accompany the article is available in the &lt;a href="https://github.com/pretzelhammer/using-rust-in-non-rust-servers"&gt;using-rust-in-non-rust-servers&lt;/a&gt; repository.

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


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



</summary><category term="javascript"/><category term="nodejs"/><category term="performance"/><category term="scaling"/><category term="rust"/></entry><entry><title>Supercharge the One Person Framework with SQLite: Rails World 2024</title><link href="https://simonwillison.net/2024/Oct/16/sqlite-rails/#atom-tag" rel="alternate"/><published>2024-10-16T22:24:45+00:00</published><updated>2024-10-16T22:24:45+00:00</updated><id>https://simonwillison.net/2024/Oct/16/sqlite-rails/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fractaledmind.github.io/2024/10/16/sqlite-supercharges-rails/"&gt;Supercharge the One Person Framework with SQLite: Rails World 2024&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Stephen Margheim shares an annotated transcript of the &lt;a href="https://www.youtube.com/watch?v=l56IBad-5aQ"&gt;YouTube video&lt;/a&gt; of his recent talk at this year's Rails World conference in Toronto.&lt;/p&gt;
&lt;p&gt;The Rails community is leaning &lt;em&gt;hard&lt;/em&gt; into SQLite right now. Stephen's talk is some of the most effective evangelism I've seen anywhere for SQLite as a production database for web applications, highlighting several new changes &lt;a href="https://simonwillison.net/2024/Oct/7/whats-new-in-ruby-on-rails-8/"&gt;in Rails 8&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;... there are two additions coming with Rails 8 that merit closer consideration. Because these changes make Rails 8 the first version of Rails (and, as far as I know, the first version of any web framework) that provides a fully production-ready SQLite experience out-of-the-box. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those changes: &lt;a href="https://github.com/rails/rails/pull/50371"&gt;Ensure SQLite transaction default to IMMEDIATE mode&lt;/a&gt; to avoid "database is locked" errors when a deferred transaction attempts to upgrade itself with a write lock (discussed here &lt;a href="https://simonwillison.net/2024/Mar/31/optimizing-sqlite-for-servers/"&gt;previously&lt;/a&gt;, and added to Datasette 1.0a14 &lt;a href="https://simonwillison.net/2024/Aug/5/datasette-1a14/#sqlite-isolation-level-immediate-"&gt;in August&lt;/a&gt;) and &lt;a href="https://github.com/rails/rails/pull/51958"&gt;SQLite non-GVL-blocking, fair retry interval busy handler&lt;/a&gt; - a lower-level change that ensures SQLite's busy handler doesn't hold Ruby's Global VM Lock (the Ruby version of Python's GIL) while a thread is waiting on a SQLite lock.&lt;/p&gt;
&lt;p&gt;The rest of the talk makes a passionate and convincing case for SQLite as an option for production deployments, in line with the Rails goal of being a &lt;a href="https://world.hey.com/dhh/the-one-person-framework-711e6318"&gt;One Person Framework&lt;/a&gt; - "a toolkit so powerful that it allows a single individual to create modern applications upon which they might build a competitive business".&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated slide. The text Single-machine SQLite-only deployments can't serve production workloads is stamped with a big red Myth stamp" src="https://static.simonwillison.net/static/2024/sqlite-myth-smaller.gif" /&gt;&lt;/p&gt;
&lt;p&gt;Back in April Stephen published &lt;a href="https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/"&gt;SQLite on Rails: The how and why of optimal performance&lt;/a&gt; describing some of these challenges in more detail (including the best explanation I've seen anywhere of &lt;code&gt;BEGIN IMMEDIATE TRANSACTION&lt;/code&gt;) and promising:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unfortunately, running SQLite on Rails out-of-the-box isn’t viable today. But, with a bit of tweaking and fine-tuning, you can ship a very performant, resilient Rails application with SQLite. And my personal goal for Rails 8 is to make the out-of-the-box experience fully production-ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It looks like he achieved that goal!

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/gil"&gt;gil&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &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/sqlite-busy"&gt;sqlite-busy&lt;/a&gt;&lt;/p&gt;



</summary><category term="gil"/><category term="rails"/><category term="ruby"/><category term="scaling"/><category term="sqlite"/><category term="sqlite-busy"/></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>Serving a billion web requests with boring code</title><link href="https://simonwillison.net/2024/Jun/28/boring-code/#atom-tag" rel="alternate"/><published>2024-06-28T16:22:45+00:00</published><updated>2024-06-28T16:22:45+00:00</updated><id>https://simonwillison.net/2024/Jun/28/boring-code/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://notes.billmill.org/blog/2024/06/Serving_a_billion_web_requests_with_boring_code.html"&gt;Serving a billion web requests with boring code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Bill Mill provides a deep retrospective from his work helping build a relaunch of the &lt;a href="https://www.medicare.gov/plan-compare/"&gt;medicare.gov/plan-compare&lt;/a&gt; site.&lt;/p&gt;
&lt;p&gt;It's a fascinating case study of the &lt;a href="https://boringtechnology.club/"&gt;choose boring technology&lt;/a&gt; mantra put into action. The "boring" choices here were PostgreSQL, Go and React, all three of which are so widely used and understood at this point that you're very unlikely to stumble into surprises with them.&lt;/p&gt;
&lt;p&gt;Key goals for the site were accessibility, in terms of users, devices and performance. Despite best efforts:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The result fell prey after a few years to a common failure mode of react apps, and became quite heavy and loaded somewhat slowly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've seen this pattern myself many times over, and I'd love to understand why. React itself isn't a particularly large dependency but somehow it always seems to lead to architectural bloat over time. Maybe that's more of an SPA thing than something that's specific to React.&lt;/p&gt;
&lt;p&gt;Loads of other interesting details in here. The ETL details - where brand new read-only RDS databases were spun up every morning after a four hour build process - are particularly notable.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/icigm4/serving_billion_web_requests_with_boring"&gt;Lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/accessibility"&gt;accessibility&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/react"&gt;react&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/boring-technology"&gt;boring-technology&lt;/a&gt;&lt;/p&gt;



</summary><category term="accessibility"/><category term="go"/><category term="postgresql"/><category term="scaling"/><category term="react"/><category term="boring-technology"/></entry><entry><title>Quoting Richard Schneeman</title><link href="https://simonwillison.net/2024/May/16/richard-schneeman/#atom-tag" rel="alternate"/><published>2024-05-16T05:44:39+00:00</published><updated>2024-05-16T05:44:39+00:00</updated><id>https://simonwillison.net/2024/May/16/richard-schneeman/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://lobste.rs/s/g9e3c1/heroku_on_two_standard_dynos#c_jj38of"&gt;&lt;p&gt;[...] by default Heroku will spin up multiple dynos in different availability zones. It also has multiple routers in different zones so if one zone should go completely offline, having a second dyno will mean that your app can still serve traffic.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://lobste.rs/s/g9e3c1/heroku_on_two_standard_dynos#c_jj38of"&gt;Richard Schneeman&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/heroku"&gt;heroku&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="heroku"/></entry><entry><title>How Figma’s databases team lived to tell the scale</title><link href="https://simonwillison.net/2024/Mar/14/how-figmas-databases-team-lived-to-tell-the-scale/#atom-tag" rel="alternate"/><published>2024-03-14T21:23:37+00:00</published><updated>2024-03-14T21:23:37+00:00</updated><id>https://simonwillison.net/2024/Mar/14/how-figmas-databases-team-lived-to-tell-the-scale/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.figma.com/blog/how-figmas-databases-team-lived-to-tell-the-scale/"&gt;How Figma’s databases team lived to tell the scale&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The best kind of scaling war story:&lt;/p&gt;
&lt;p&gt;"Figma’s database stack has grown almost 100x since 2020. [...] In 2020, we were running a single Postgres database hosted on AWS’s largest physical instance, and by the end of 2022, we had built out a distributed architecture with caching, read replicas, and a dozen vertically partitioned databases."&lt;/p&gt;
&lt;p&gt;I like the concept of "colos", their internal name for sharded groups of related tables arranged such that those tables can be queried using joins.&lt;/p&gt;
&lt;p&gt;Also smart: separating the migration into "logical sharding" - where queries all still run against a single database, even though they are logically routed as if the database was already sharded - followed by "physical sharding" where the data is actually copied to and served from the new database servers.&lt;/p&gt;
&lt;p&gt;Logical sharding was implemented using PostgreSQL views, which can accept both reads and writes:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CREATE VIEW table_shard1 AS SELECT * FROM table
WHERE hash(shard_key) &amp;gt;= min_shard_range AND hash(shard_key) &amp;lt; max_shard_range)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The final piece of the puzzle was DBProxy, a custom PostgreSQL query proxy written in Go that can parse the query to an AST and use that to decide which shard the query should be sent to. Impressively it also has a scatter-gather mechanism, so &lt;code&gt;select * from table&lt;/code&gt; can be sent to all shards at once and the results combined back together again.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sharding"&gt;sharding&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="postgresql"/><category term="scaling"/><category term="sharding"/></entry><entry><title>The power of two random choices, visualized</title><link href="https://simonwillison.net/2024/Feb/6/the-power-of-two-random-choices-visualized/#atom-tag" rel="alternate"/><published>2024-02-06T22:21:47+00:00</published><updated>2024-02-06T22:21:47+00:00</updated><id>https://simonwillison.net/2024/Feb/6/the-power-of-two-random-choices-visualized/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://twitter.com/grantslatton/status/1754912113246798036"&gt;The power of two random choices, visualized&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Grant Slatton shares a visualization illustrating “a favorite load balancing technique at AWS”: pick two nodes at random and then send the task to whichever of those two has the lowest current load score.&lt;/p&gt;

&lt;p&gt;Why just two nodes? “The function grows logarithmically, so it’s a big jump from 1 to 2 and then tapers off *real* quick.”


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aws"&gt;aws&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/load-balancing"&gt;load-balancing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;&lt;/p&gt;



</summary><category term="aws"/><category term="load-balancing"/><category term="scaling"/></entry><entry><title>Database “sharding” came from UO?</title><link href="https://simonwillison.net/2023/Apr/7/database-sharding-came-from-ultima-online/#atom-tag" rel="alternate"/><published>2023-04-07T13:56:17+00:00</published><updated>2023-04-07T13:56:17+00:00</updated><id>https://simonwillison.net/2023/Apr/7/database-sharding-came-from-ultima-online/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.raphkoster.com/2009/01/08/database-sharding-came-from-uo/"&gt;Database “sharding” came from UO?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Raph Koster coined the term “shard” back in 1996 in a design document proposing a way of scaling Ultima Online: “[...] we realized we would need to run multiple whole copies of Ultima Online for users to connect to, we needed to come up with a fiction for it. [...] the evil wizard Mondain had attempted to gain control over Sosaria by trapping its essence in a crystal. When the Stranger at the end of Ultima I defeated Mondain and shattered the crystal, the crystal shards each held a refracted copy of Sosaria.”

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=35479553"&gt;Hacker News&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/sharding"&gt;sharding&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="sharding"/></entry><entry><title>How Discord Stores Trillions of Messages</title><link href="https://simonwillison.net/2023/Mar/8/how-discord-stores-trillions-of-messages/#atom-tag" rel="alternate"/><published>2023-03-08T19:07:08+00:00</published><updated>2023-03-08T19:07:08+00:00</updated><id>https://simonwillison.net/2023/Mar/8/how-discord-stores-trillions-of-messages/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://discord.com/blog/how-discord-stores-trillions-of-messages"&gt;How Discord Stores Trillions of Messages&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is a really interesting case-study. Discord migrated from MongoDB to Cassandra &lt;a href="https://simonwillison.net/2021/Aug/24/how-discord-stores-billions-of-messages/"&gt;back in 2016&lt;/a&gt; to handle billions of messages. Today they're handling trillions, and they completed a migration from Cassandra to Scylla, a Cassandra-like data store written in C++ (as opposed to Cassandra's Java) to help avoid problems like GC pauses. In addition to being a really good scaling war story this has some interesting details about their increased usage of Rust. As a fan of request coalescing (which I've previously referred to as dogpile prevention) I particularly liked this bit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our data services sit between the API and our ScyllaDB clusters. They contain roughly one gRPC endpoint per database query and intentionally contain no business logic. The big feature our data services provide is request coalescing. If multiple users are requesting the same row at the same time, we’ll only query the database once. The first user that makes a request causes a worker task to spin up in the service. Subsequent requests will check for the existence of that task and subscribe to it. That worker task will query the database and return the row to all subscribers.&lt;/p&gt;
&lt;/blockquote&gt;

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


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



</summary><category term="cassandra"/><category term="dogpile"/><category term="nosql"/><category term="scaling"/><category term="rust"/><category term="discord"/></entry><entry><title>Scaling Mastodon: The Compendium</title><link href="https://simonwillison.net/2022/Nov/29/scaling-mastodon-the-compendium/#atom-tag" rel="alternate"/><published>2022-11-29T05:46:03+00:00</published><updated>2022-11-29T05:46:03+00:00</updated><id>https://simonwillison.net/2022/Nov/29/scaling-mastodon-the-compendium/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://hazelweakly.me/blog/scaling-mastodon/"&gt;Scaling Mastodon: The Compendium&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Hazel Weakly’s collection of notes on scaling Mastodon, covering PostgreSQL, Sidekiq, Redis, object storage and more.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://hachyderm.io/@nova/109422755533605556"&gt;hachyderm.io/@nova&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/redis"&gt;redis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mastodon"&gt;mastodon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sidekiq"&gt;sidekiq&lt;/a&gt;&lt;/p&gt;



</summary><category term="postgresql"/><category term="redis"/><category term="scaling"/><category term="mastodon"/><category term="sidekiq"/></entry><entry><title>Transactionally Staged Job Drains in Postgres</title><link href="https://simonwillison.net/2021/Dec/18/transactionally-staged-job-drains/#atom-tag" rel="alternate"/><published>2021-12-18T01:34:13+00:00</published><updated>2021-12-18T01:34:13+00:00</updated><id>https://simonwillison.net/2021/Dec/18/transactionally-staged-job-drains/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://brandur.org/job-drain"&gt;Transactionally Staged Job Drains in Postgres&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Any time I see people argue that relational databases shouldn’t be used to implement job queues I think of this post by Brandur from 2017. If you write to a queue before committing a transaction you run the risk of a queue consumer trying to read from the database before the new row becomes visible. If you write to the queue after the transaction there’s a risk an error might result in your message never being written. So: write to a relational staging table as part of the transaction, then have a separate process read from that table and write to the queue.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/queues"&gt;queues&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/brandur-leach"&gt;brandur-leach&lt;/a&gt;&lt;/p&gt;



</summary><category term="postgresql"/><category term="queues"/><category term="scaling"/><category term="brandur-leach"/></entry><entry><title>Centrifuge: a reliable system for delivering billions of events per day</title><link href="https://simonwillison.net/2021/Dec/6/centrifue/#atom-tag" rel="alternate"/><published>2021-12-06T01:41:54+00:00</published><updated>2021-12-06T01:41:54+00:00</updated><id>https://simonwillison.net/2021/Dec/6/centrifue/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://segment.com/blog/introducing-centrifuge/"&gt;Centrifuge: a reliable system for delivering billions of events per day&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
From 2018, a write-up from Segment explaining how they solved the problem of delivering webhooks from thousands of different producers to hundreds of potentially unreliable endpoints. They started with Kafka and ended up on a custom system written in Go against RDS MySQL that was specifically tuned to their write-heavy requirements.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/jkakar/status/1467653837917863936"&gt;@jkakar&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/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/segment"&gt;segment&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="webhooks"/><category term="segment"/></entry><entry><title>How Discord Stores Billions of Messages</title><link href="https://simonwillison.net/2021/Aug/24/how-discord-stores-billions-of-messages/#atom-tag" rel="alternate"/><published>2021-08-24T21:31:36+00:00</published><updated>2021-08-24T21:31:36+00:00</updated><id>https://simonwillison.net/2021/Aug/24/how-discord-stores-billions-of-messages/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.discord.com/how-discord-stores-billions-of-messages-7fa6ec7ee4c7"&gt;How Discord Stores Billions of Messages&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fascinating article from 2017 describing how Discord migrated their primary message store to Cassandra (from MongoDB, but I could easily see them making the same decision if they had started with PostgreSQL or MySQL).&lt;/p&gt;
&lt;p&gt;The trick with scalable NoSQL databases like Cassandra is that you need to have a very deep understanding of the kinds of queries you will need to answer - and Discord had exactly that.&lt;/p&gt;
&lt;p&gt;In the article they talk about their desire to eventually migrate to Scylla (a compatible Cassandra alternative written in C++) - in the Hacker News comments &lt;a href="https://news.ycombinator.com/item?id=28292369#28293844"&gt;they confirm&lt;/a&gt; that in 2021 they are using Scylla for a few things but they still have their core messages in Cassandra.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cassandra"&gt;cassandra&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nosql"&gt;nosql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/discord"&gt;discord&lt;/a&gt;&lt;/p&gt;



</summary><category term="cassandra"/><category term="nosql"/><category term="scaling"/><category term="discord"/></entry><entry><title>Notes on streaming large API responses</title><link href="https://simonwillison.net/2021/Jun/25/streaming-large-api-responses/#atom-tag" rel="alternate"/><published>2021-06-25T16:26:49+00:00</published><updated>2021-06-25T16:26:49+00:00</updated><id>https://simonwillison.net/2021/Jun/25/streaming-large-api-responses/#atom-tag</id><summary type="html">
    &lt;p&gt;I started &lt;a href="https://twitter.com/simonw/status/1405554676993433605"&gt;a Twitter conversation&lt;/a&gt; last week about API endpoints that stream large amounts of data as an alternative to APIs that return 100 results at a time and require clients to paginate through all of the pages in order to retrieve all of the data:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p dir="ltr" lang="en"&gt;Any unexpected downsides to offering streaming HTTP API endpoints that serve up eg 100,000 JSON objects in a go rather than asking users to paginate 100 at a time over 1,000 requests, assuming efficient implementation of that streaming endpoint?&lt;/p&gt;— Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1405554676993433605?ref_src=twsrc%5Etfw"&gt;June 17, 2021&lt;/a&gt;
&lt;/blockquote&gt;
&lt;p&gt;I got a ton of great replies. I tried to tie them together in a thread attached to the tweet, but I'm also going to synthesize them into some thoughts here.&lt;/p&gt;
&lt;h4&gt;Bulk exporting data&lt;/h4&gt;
&lt;p&gt;The more time I spend with APIs, especially with regard to my &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; and &lt;a href="https://simonwillison.net/2020/Nov/14/personal-data-warehouses/"&gt;Dogsheep&lt;/a&gt; projects, the more I realize that my favourite APIs are the ones that let you extract &lt;em&gt;all&lt;/em&gt; of your data as quickly and easily as possible.&lt;/p&gt;
&lt;p&gt;There are generally three ways an API might provide this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click an "export everything" button, then wait for a while for an email to show up with a link to a downloadable zip file. This isn't really an API, in particular since it's usually hard if not impossible to automate that initial "click", but it's still better than nothing. Google's &lt;a href="https://takeout.google.com/"&gt;Takeout&lt;/a&gt; is one notable implementation of this pattern.&lt;/li&gt;
&lt;li&gt;Provide a JSON API which allows users to paginate through their data. This is a very common pattern, although it can run into difficulties: what happens if new data is added while you are paginating through the original data, for example? Some systems only allow access to the first N pages too, for performance reasons.&lt;/li&gt;
&lt;li&gt;Providing a single HTTP endpoint you can hit that will return ALL of your data - potentially dozens or hundreds of MBs of it - in one go.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's that last option that I'm interested in talking about today.&lt;/p&gt;
&lt;h4&gt;Efficiently streaming data&lt;/h4&gt;
&lt;p&gt;It used to be that most web engineers would quickly discount the idea of an API endpoint that streams out an unlimited number of rows. HTTP requests should be served as quickly as possible! Anything more than a couple of seconds spent processing a request is a red flag that something should be reconsidered.&lt;/p&gt;
&lt;p&gt;Almost everything in the web stack is optimized for quickly serving small requests. But over the past decade the tide has turned somewhat: Node.js made async web servers commonplace, WebSockets taught us to handle long-running connections and in the Python world asyncio and &lt;a href="https://asgi.readthedocs.io/"&gt;ASGI&lt;/a&gt; provided a firm foundation for handling long-running requests using smaller amounts of RAM and CPU.&lt;/p&gt;
&lt;p&gt;I've been experimenting in this area for a few years now.&lt;/p&gt;
&lt;p&gt;Datasette has the ability to &lt;a href="https://github.com/simonw/datasette/blob/0.57.1/datasette/views/base.py#L264-L428"&gt;use ASGI trickery&lt;/a&gt; to &lt;a href="https://docs.datasette.io/en/stable/csv_export.html#streaming-all-records"&gt;stream all rows from a table&lt;/a&gt; (or filtered table) as CSV, potentially returning hundreds of MBs of data.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://django-sql-dashboard.datasette.io/"&gt;Django SQL Dashboard&lt;/a&gt; can export the full results of a SQL query as CSV or TSV, this time using Django's &lt;a href="https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.StreamingHttpResponse"&gt;StreamingHttpResponse&lt;/a&gt; (which does tie up a full worker process, but that's OK if you restrict it to a controlled number of authenticated users).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simonwillison.net/tags/vaccinateca/"&gt;VIAL&lt;/a&gt; implements streaming responses to offer an &lt;a href="https://github.com/CAVaccineInventory/vial/blob/cdaaab053a9cf1cef40104a2cdf480b7932d58f7/vaccinate/core/admin_actions.py"&gt;"export from the admin" feature&lt;/a&gt;. It also has an API-key-protected search API which can stream out all matching rows &lt;a href="https://github.com/CAVaccineInventory/vial/blob/cdaaab053a9cf1cef40104a2cdf480b7932d58f7/vaccinate/api/serialize.py#L38"&gt;in JSON or GeoJSON&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Implementation notes&lt;/h4&gt;
&lt;p&gt;The key thing to watch out for when implementing this pattern is memory usage: if your server buffers 100MB+ of data any time it needs to serve an export request you're going to run into trouble.&lt;/p&gt;
&lt;p&gt;Some export formats are friendlier for streaming than others. CSV and TSV are pretty easy to stream, as is newline-delimited JSON.&lt;/p&gt;
&lt;p&gt;Regular JSON requires a bit more thought: you can output a &lt;code&gt;[&lt;/code&gt; character, then output each row in a stream with a comma suffix, then skip the comma for the last row and output a &lt;code&gt;]&lt;/code&gt;. Doing that requires peeking ahead (looping two at a time) to verify that you haven't yet reached the end.&lt;/p&gt;
&lt;p&gt;Or... Martin De Wulf &lt;a href="https://twitter.com/madewulf/status/1405559088994467844"&gt;pointed out&lt;/a&gt; that you can output the first row, then output every other row with a preceeding comma - which avoids the whole "iterate two at a time" problem entirely.&lt;/p&gt;
&lt;p&gt;The next challenge is efficiently looping through every database result without first pulling them all into memory.&lt;/p&gt;
&lt;p&gt;PostgreSQL (and the &lt;code&gt;psycopg2&lt;/code&gt; Python module) offers &lt;a href="https://www.psycopg.org/docs/usage.html#server-side-cursors"&gt;server-side cursors&lt;/a&gt;, which means you can stream results through your code without loading them all at once. I use these &lt;a href="https://github.com/simonw/django-sql-dashboard/blob/dd1bb18e45b40ce8f3d0553a72b7ec3cdc329e69/django_sql_dashboard/views.py#L397-L399"&gt;in Django SQL Dashboard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Server-side cursors make me nervous though, because they seem like they likely tie up resources in the database itself. So the other technique I would consider here is &lt;a href="https://use-the-index-luke.com/no-offset"&gt;keyset pagination&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Keyset pagination works against any data that is ordered by a unique column - it works especially well against a primary key (or other indexed column). Each page of data is retrieved using a query something like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; items &lt;span class="pl-k"&gt;order by&lt;/span&gt; id &lt;span class="pl-k"&gt;limit&lt;/span&gt; &lt;span class="pl-c1"&gt;21&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the &lt;code&gt;limit 21&lt;/code&gt; - if we are retrieving pages of 20 items we ask for 21, since then we can use the last returned item to tell if there is a next page or not.&lt;/p&gt;
&lt;p&gt;Then for subsequent pages take the 20th &lt;code&gt;id&lt;/code&gt; value and ask for things greater than that:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; items &lt;span class="pl-k"&gt;where&lt;/span&gt; id &lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;20&lt;/span&gt; &lt;span class="pl-k"&gt;limit&lt;/span&gt; &lt;span class="pl-c1"&gt;21&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each of these queries is fast to respond (since it's against an ordered index) and uses a predictable, fixed amount of memory. Using keyset pagination we can loop through an abitrarily large table of data, streaming each page out one at a time, without exhausting any resources.&lt;/p&gt;
&lt;p&gt;And since each query is small and fast, we don't need to worry about huge queries tying up database resources either.&lt;/p&gt;
&lt;h4&gt;What can go wrong?&lt;/h4&gt;
&lt;p&gt;I really like these patterns. They haven't bitten me yet, though I've not deployed them for anything truly huge scale. So I &lt;a href="https://twitter.com/simonw/status/1405554676993433605"&gt;asked Twitter&lt;/a&gt; what kind of problems I should look for.&lt;/p&gt;
&lt;p&gt;Based on the Twitter conversation, here are some of the challenges that this approach faces.&lt;/p&gt;
&lt;h4&gt;Challenge: restarting servers&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p dir="ltr" lang="en"&gt;If the stream takes a significantly long time to finish then rolling out updates becomes a problem. You don't want to interrupt a download but also don't want to wait forever for it to finish to spin down the server.&lt;/p&gt;— Adam Lowry (@robotadam) &lt;a href="https://twitter.com/robotadam/status/1405556544897384459?ref_src=twsrc%5Etfw"&gt;June 17, 2021&lt;/a&gt;
&lt;/blockquote&gt;
&lt;p&gt;This came up a few times, and is something I hadn't considered. If your deployment process involves restarting your servers (and it's hard to imagine one that doesn't) you need to take long-running connections into account when you do that. If there's a user half way through a 500MB stream you can either truncate their connection or wait for them to finish.&lt;/p&gt;
&lt;h4 id="challenge-errors"&gt;Challenge: how to return errors&lt;/h4&gt;
&lt;p&gt;If you're streaming a response, you start with an HTTP 200 code... but then what happens if an error occurs half-way through, potentially while paginating through the database?&lt;/p&gt;
&lt;p&gt;You've already started sending the request, so you can't change the status code to a 500. Instead, you need to write some kind of error to the stream that's being produced.&lt;/p&gt;
&lt;p&gt;If you're serving up a huge JSON document, you can at least make that JSON become invalid, which should indicate to your client that something went wrong.&lt;/p&gt;
&lt;p&gt;Formats like CSV are harder. How do you let your user know that their CSV data is incomplete?&lt;/p&gt;
&lt;p&gt;And what if someone's connection drops - are they definitely going to notice that they are missing something, or will they assume that the truncated file is all of the data?&lt;/p&gt;
&lt;h4&gt;Challenge: resumable downloads&lt;/h4&gt;
&lt;p&gt;If a user is paginating through your API, they get resumability for free: if something goes wrong they can start again at the last page that they fetched.&lt;/p&gt;
&lt;p&gt;Resuming a single stream is a lot harder.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests"&gt;HTTP range mechanism&lt;/a&gt; can be used to provide resumable downloads against large files, but it only works if you generate the entire file in advance.&lt;/p&gt;
&lt;p&gt;There is a way to design APIs to support this, provided the data in the stream is in a predictable order (which it has to be if you're using keyset pagination, described above).&lt;/p&gt;
&lt;p&gt;Have the endpoint that triggers the download take an optional &lt;code&gt;?since=&lt;/code&gt; parameter, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /stream-everything?since=b24ou34
[
    {"id": "m442ecc", "name": "..."},
    {"id": "c663qo2", "name": "..."},
    {"id": "z434hh3", "name": "..."},
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the &lt;code&gt;b24ou34&lt;/code&gt; is an identifier - it can be a deliberately opaque token, but it needs to be served up as part of the response.&lt;/p&gt;
&lt;p&gt;If the user is disconnected for any reason, they can start back where they left off by passing in the last ID that they successfully retrieved:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /stream-everything?since=z434hh3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This still requires some level of intelligence from the client application, but it's a reasonably simple pattern both to implement on the server and as a client.&lt;/p&gt;
&lt;h4&gt;Easiest solution: generate and return from cloud storage&lt;/h4&gt;
&lt;p&gt;It seems the most robust way to implement this kind of API is the least technically exciting: spin off a background task that generates the large response and pushes it to cloud storage (S3 or GCS), then redirect the user to a signed URL to download the resulting file.&lt;/p&gt;
&lt;p&gt;This is easy to scale, gives users complete files with content-length headers that they know they can download (and even resume-downloading, since range headers are supported by S3 and GCS). It also avoids any issues with server restarts caused by long connections.&lt;/p&gt;
&lt;p&gt;This is how Mixpanel handle their export feature, and it's &lt;a href="https://seancoates.com/blogs/lambda-payload-size-workaround"&gt;the solution Sean Coates came to&lt;/a&gt; when trying to find a workaround for the AWS Lambda/API Gate response size limit.&lt;/p&gt;
&lt;p&gt;If your goal is to provide your users a robust, reliable bulk-export mechanism for their data, export to cloud storage is probably the way to go.&lt;/p&gt;
&lt;p&gt;But streaming dynamic responses are a really neat trick, and I plan to keep exploring them!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/streaming"&gt;streaming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&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="apis"/><category term="http"/><category term="scaling"/><category term="streaming"/><category term="asgi"/><category term="http-range-requests"/></entry><entry><title>Multi-region PostgreSQL on Fly</title><link href="https://simonwillison.net/2021/Jun/17/multi-region-postgresql-on-fly/#atom-tag" rel="alternate"/><published>2021-06-17T18:39:06+00:00</published><updated>2021-06-17T18:39:06+00:00</updated><id>https://simonwillison.net/2021/Jun/17/multi-region-postgresql-on-fly/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fly.io/docs/getting-started/multi-region-databases/"&gt;Multi-region PostgreSQL on Fly&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Really interesting piece of architectural design from Fly here. Fly can run your application (as a Docker container run using Firecracker) in multiple regions around the world, and they’ve now quietly added PostgreSQL multi-region support. The way it works is that all-but-one region can have a read-only replica, and requests sent to application servers can perform read-only queries against their local region’s replica. If a request needs to execute a SQL update your application code can return a “fly-replay: region=scl” HTTP header and the Fly CDN will transparently replay the request against the region containing the leader database. This also means you can implement tricks like setting a 10s expiring cookie every time the user performs a write, such that their requests in the next 10s will go straight to the leader and avoid them experiencing any replication lag that hasn’t caught up with their latest update.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/replication"&gt;replication&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;&lt;/p&gt;



</summary><category term="postgresql"/><category term="replication"/><category term="scaling"/><category term="fly"/></entry><entry><title>Why I Built Litestream</title><link href="https://simonwillison.net/2021/Feb/11/why-i-built-litestream/#atom-tag" rel="alternate"/><published>2021-02-11T19:25:26+00:00</published><updated>2021-02-11T19:25:26+00:00</updated><id>https://simonwillison.net/2021/Feb/11/why-i-built-litestream/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://litestream.io/blog/why-i-built-litestream/"&gt;Why I Built Litestream&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Litestream is a really exciting new piece of technology by Ben Johnson, who previously built BoltDB, the key-value store written in Go that is used by etcd. It adds replication to SQLite by running a process that converts the SQLite WAL log into a stream that can be saved to another folder or pushed to S3. The S3 option is particularly exciting—Ben estimates that keeping a full point-in-time recovery log of a high write SQLite database should cost in the order of a few dollars a month. I think this could greatly expand the set of use-cases for which SQLite is sensible choice.


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



</summary><category term="replication"/><category term="scaling"/><category term="sqlite"/><category term="ben-johnson"/></entry></feed>