<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: gzip</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/gzip.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-04-09T03:54:40+00:00</updated><author><name>Simon Willison</name></author><entry><title>asgi-gzip 0.3</title><link href="https://simonwillison.net/2026/Apr/9/asgi-gzip/#atom-tag" rel="alternate"/><published>2026-04-09T03:54:40+00:00</published><updated>2026-04-09T03:54:40+00:00</updated><id>https://simonwillison.net/2026/Apr/9/asgi-gzip/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/asgi-gzip/releases/tag/0.3"&gt;asgi-gzip 0.3&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;I ran into trouble deploying a new feature using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events"&gt;SSE&lt;/a&gt; to a production Datasette instance, and it turned out that instance was using &lt;a href="https://github.com/simonw/datasette-gzip"&gt;datasette-gzip&lt;/a&gt; which uses &lt;a href="https://github.com/simonw/asgi-gzip"&gt;asgi-gzip&lt;/a&gt; which was incorrectly compressing &lt;code&gt;event/text-stream&lt;/code&gt; responses.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asgi-gzip&lt;/code&gt; was extracted from Starlette, and has &lt;a href="https://simonwillison.net/2022/Apr/28/issue-on-changes/"&gt;a GitHub Actions scheduled workflow&lt;/a&gt; to check Starlette for updates that need to be ported to the library... but that action had stopped running and hence had missed &lt;a href="https://github.com/Kludex/starlette/commit/a9a8dab0cc3cbd05dca37650fc392717b9fe5bbf"&gt;Starlette's own fix&lt;/a&gt; for this issue.&lt;/p&gt;
&lt;p&gt;I ran the workflow and integrated the new fix, and now &lt;code&gt;datasette-gzip&lt;/code&gt; and &lt;code&gt;asgi-gzip&lt;/code&gt; both correctly handle &lt;code&gt;text/event-stream&lt;/code&gt; in SSE responses.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="gzip"/><category term="python"/><category term="asgi"/></entry><entry><title>Automatically opening issues when tracked file content changes</title><link href="https://simonwillison.net/2022/Apr/28/issue-on-changes/#atom-tag" rel="alternate"/><published>2022-04-28T17:18:14+00:00</published><updated>2022-04-28T17:18:14+00:00</updated><id>https://simonwillison.net/2022/Apr/28/issue-on-changes/#atom-tag</id><summary type="html">
    &lt;p&gt;I figured out a GitHub Actions pattern to keep track of a file published somewhere on the internet and automatically open a new repository issue any time the contents of that file changes.&lt;/p&gt;
&lt;h4&gt;Extracting GZipMiddleware from Starlette&lt;/h4&gt;
&lt;p&gt;Here's why I needed to solve this problem.&lt;/p&gt;
&lt;p&gt;I want to add gzip support to my &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; open source project. Datasette builds on the Python &lt;a href="https://asgi.readthedocs.io/"&gt;ASGI&lt;/a&gt; standard, and &lt;a href="https://www.starlette.io/"&gt;Starlette&lt;/a&gt; provides an extremely well tested, robust &lt;a href="https://www.starlette.io/middleware/#gzipmiddleware"&gt;GZipMiddleware class&lt;/a&gt; that adds gzip support to any ASGI application. As with everything else in Starlette, it's &lt;em&gt;really&lt;/em&gt; good code.&lt;/p&gt;
&lt;p&gt;The problem is, I don't want to add the whole of Starlette as a dependency. I'm trying to keep Datasette's core as small as possible, so I'm very careful about new dependencies. Starlette itself is actually very light (and only has a tiny number of dependencies of its own) but I still don't want the whole thing just for that one class.&lt;/p&gt;
&lt;p&gt;So I decided to extract the &lt;code&gt;GZipMiddleware&lt;/code&gt; class into a separate Python package, under the same BSD license as Starlette itself.&lt;/p&gt;
&lt;p&gt;The result is my new &lt;a href="https://pypi.org/project/asgi-gzip/"&gt;asgi-gzip&lt;/a&gt; package, now available on PyPI.&lt;/p&gt;
&lt;h4&gt;What if Starlette fixes a bug?&lt;/h4&gt;
&lt;p&gt;The problem with extracting code like this is that Starlette is a very effectively maintained package. What if they make improvements or fix bugs in the &lt;code&gt;GZipMiddleware&lt;/code&gt; class? How can I make sure to apply those same fixes to my extracted copy?&lt;/p&gt;
&lt;p&gt;As I thought about this challenge, I realized I had most of the solution already.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonwillison.net/2020/Oct/9/git-scraping/"&gt;Git scraping&lt;/a&gt;&lt;/strong&gt; is the name I've given to the trick of running a periodic scraper that writes to a git repository in order to track changes to data over time.&lt;/p&gt;
&lt;p&gt;It may seem redundant to do this against a file that already &lt;a href="https://github.com/encode/starlette/commits/master/starlette/middleware/gzip.py"&gt;lives in version control&lt;/a&gt; elsewhere - but in addition to tracking changes, Git scraping can offfer a cheap and easy way to add automation that triggers when a change is detected.&lt;/p&gt;
&lt;p&gt;I need an actionable alert any time the Starlette code changes so I can review the change and apply a fix to my own library, if necessary.&lt;/p&gt;
&lt;p&gt;Since I already run all of my projects out of GitHub issues, automatically opening an issue against the &lt;a href="https://github.com/simonw/asgi-gzip"&gt;asgi-gzip repository&lt;/a&gt; would be ideal.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/asgi-gzip/blob/0.1/.github/workflows/track.yml"&gt;track.yml workflow&lt;/a&gt; does exactly that: it implements the Git scraping pattern against the &lt;a href="https://github.com/encode/starlette/blob/master/starlette/middleware/gzip.py"&gt;gzip.py module&lt;/a&gt; in Starlette, and files an issue any time it detects changes to that file.&lt;/p&gt;
&lt;p&gt;Starlette haven't made any changes to that file since I started tracking it, so I created &lt;a href="https://github.com/simonw/issue-when-changed"&gt;a test repo&lt;/a&gt; to try this out.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/issue-when-changed/issues/3"&gt;one of the example issues&lt;/a&gt;. I decided to include the visual diff in the issue description and have a link to it from the underlying commit as well.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/issue-when-changed.jpg" alt="Screenshot of an open issue page. The issues is titled &amp;quot;gzip.py was updated&amp;quot; and contains a visual diff showing the change to a file. A commit that references the issue is listed too." style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;How it works&lt;/h4&gt;
&lt;p&gt;The implementation is contained entirely in this &lt;a href="https://github.com/simonw/asgi-gzip/blob/0.1/.github/workflows/track.yml"&gt;track.yml workflow&lt;/a&gt;. I designed this to be contained as a single file to make it easy to copy and paste it to adapt it for other projects.&lt;/p&gt;
&lt;p&gt;It uses &lt;a href="https://github.com/actions/github-script"&gt;actions/github-script&lt;/a&gt;, which makes it easy to do things like file new issues using JavaScript.&lt;/p&gt;
&lt;p&gt;Here's a heavily annotated copy:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Track the Starlette version of this&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Run on repo pushes, and if a user clicks the "run this action" button,&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; and on a schedule at 5:21am UTC every day&lt;/span&gt;
&lt;span class="pl-ent"&gt;on&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;push&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;workflow_dispatch&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;schedule&lt;/span&gt;:
  - &lt;span class="pl-ent"&gt;cron&lt;/span&gt;:  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;21 5 * * *&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Without this block I got this error when the action ran:&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; HttpError: Resource not accessible by integration&lt;/span&gt;
&lt;span class="pl-ent"&gt;permissions&lt;/span&gt;:
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Allow the action to create issues&lt;/span&gt;
  &lt;span class="pl-ent"&gt;issues&lt;/span&gt;: &lt;span class="pl-s"&gt;write&lt;/span&gt;
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Allow the action to commit back to the repository&lt;/span&gt;
  &lt;span class="pl-ent"&gt;contents&lt;/span&gt;: &lt;span class="pl-s"&gt;write&lt;/span&gt;

&lt;span class="pl-ent"&gt;jobs&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;check&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
    - &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/checkout@v2&lt;/span&gt;
    - &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/github-script@v6&lt;/span&gt;
      &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Using env: here to demonstrate how an action like this can&lt;/span&gt;
      &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; be adjusted to take dynamic inputs&lt;/span&gt;
      &lt;span class="pl-ent"&gt;env&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;URL&lt;/span&gt;: &lt;span class="pl-s"&gt;https://raw.githubusercontent.com/encode/starlette/master/starlette/middleware/gzip.py&lt;/span&gt;
        &lt;span class="pl-ent"&gt;FILE_NAME&lt;/span&gt;: &lt;span class="pl-s"&gt;tracking/gzip.py&lt;/span&gt;
      &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;script&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;          const { URL, FILE_NAME } = process.env;&lt;/span&gt;
&lt;span class="pl-s"&gt;          // promisify pattern for getting an await version of child_process.exec&lt;/span&gt;
&lt;span class="pl-s"&gt;          const util = require("util");&lt;/span&gt;
&lt;span class="pl-s"&gt;          // Used exec_ here because 'exec' variable name is already used:&lt;/span&gt;
&lt;span class="pl-s"&gt;          const exec_ = util.promisify(require("child_process").exec);&lt;/span&gt;
&lt;span class="pl-s"&gt;          // Use curl to download the file&lt;/span&gt;
&lt;span class="pl-s"&gt;          await exec_(`curl -o ${FILE_NAME} ${URL}`);&lt;/span&gt;
&lt;span class="pl-s"&gt;          // Use 'git diff' to detect if the file has changed since last time&lt;/span&gt;
&lt;span class="pl-s"&gt;          const { stdout } = await exec_(`git diff ${FILE_NAME}`);&lt;/span&gt;
&lt;span class="pl-s"&gt;          if (stdout) {&lt;/span&gt;
&lt;span class="pl-s"&gt;            // There was a diff to that file&lt;/span&gt;
&lt;span class="pl-s"&gt;            const title = `${FILE_NAME} was updated`;&lt;/span&gt;
&lt;span class="pl-s"&gt;            const body =&lt;/span&gt;
&lt;span class="pl-s"&gt;              `${URL} changed:` +&lt;/span&gt;
&lt;span class="pl-s"&gt;              "\n\n```diff\n" +&lt;/span&gt;
&lt;span class="pl-s"&gt;              stdout +&lt;/span&gt;
&lt;span class="pl-s"&gt;              "\n```\n\n" +&lt;/span&gt;
&lt;span class="pl-s"&gt;              "Close this issue once those changes have been integrated here";&lt;/span&gt;
&lt;span class="pl-s"&gt;            const issue = await github.rest.issues.create({&lt;/span&gt;
&lt;span class="pl-s"&gt;              owner: context.repo.owner,&lt;/span&gt;
&lt;span class="pl-s"&gt;              repo: context.repo.repo,&lt;/span&gt;
&lt;span class="pl-s"&gt;              title: title,&lt;/span&gt;
&lt;span class="pl-s"&gt;              body: body,&lt;/span&gt;
&lt;span class="pl-s"&gt;            });&lt;/span&gt;
&lt;span class="pl-s"&gt;            const issueNumber = issue.data.number;&lt;/span&gt;
&lt;span class="pl-s"&gt;            // Now commit and reference that issue number, so the commit shows up&lt;/span&gt;
&lt;span class="pl-s"&gt;            // listed at the bottom of the issue page&lt;/span&gt;
&lt;span class="pl-s"&gt;            const commitMessage = `${FILE_NAME} updated, refs #${issueNumber}`;&lt;/span&gt;
&lt;span class="pl-s"&gt;            // https://til.simonwillison.net/github-actions/commit-if-file-changed&lt;/span&gt;
&lt;span class="pl-s"&gt;            await exec_(`git config user.name "Automated"`);&lt;/span&gt;
&lt;span class="pl-s"&gt;            await exec_(`git config user.email "actions@users.noreply.github.com"`);&lt;/span&gt;
&lt;span class="pl-s"&gt;            await exec_(`git add -A`);&lt;/span&gt;
&lt;span class="pl-s"&gt;            await exec_(`git commit -m "${commitMessage}" || exit 0`);&lt;/span&gt;
&lt;span class="pl-s"&gt;            await exec_(`git pull --rebase`);&lt;/span&gt;
&lt;span class="pl-s"&gt;            await exec_(`git push`);&lt;/span&gt;
&lt;span class="pl-s"&gt;          }&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the &lt;a href="https://github.com/simonw/asgi-gzip"&gt;asgi-gzip&lt;/a&gt; repository I keep the fetched &lt;code&gt;gzip.py&lt;/code&gt; file in a &lt;code&gt;tracking/&lt;/code&gt; directory. This directory isn't included in the Python package that gets uploaded to PyPI - it's there only so that my code can track changes to it over time.&lt;/p&gt;
&lt;h4&gt;More interesting applications&lt;/h4&gt;
&lt;p&gt;I built this to solve my "tell me when Starlette update their &lt;code&gt;gzip.py&lt;/code&gt; file" problem, but clearly this pattern has much more interesting uses.&lt;/p&gt;
&lt;p&gt;You could point this at any web page to get a new GitHub issue opened when that page content changes. Subscribe to notifications for that repository and you get a robust , shared mechanism for alerts - plus an issue system where you can post additional comments and close the issue once someone has reviewed the change.&lt;/p&gt;
&lt;p&gt;There's a lot of potential here for solving all kinds of interesting problems. And it doesn't cost anything either: GitHub Actions (somehow) remains completely free for public repositories!&lt;/p&gt;
&lt;h4&gt;Update: October 13th 2022&lt;/h4&gt;
&lt;p&gt;Almost six months after writing about this... it triggered for the first time!&lt;/p&gt;
&lt;p&gt;Here's the issue that the script opened: &lt;a href="https://github.com/simonw/asgi-gzip/issues/4"&gt;#4: tracking/gzip.py was updated&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I applied the improvement (Marcelo Trylesinski and Kai Klingenberg updated Starlette's code to avoid gzipping if the response already had a Content-Encoding header) and released &lt;a href="https://github.com/simonw/asgi-gzip/releases/tag/0.2"&gt;version 0.2&lt;/a&gt; of the package.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git-scraping"&gt;git-scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-issues"&gt;github-issues&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="gzip"/><category term="projects"/><category term="python"/><category term="datasette"/><category term="asgi"/><category term="github-actions"/><category term="git-scraping"/><category term="github-issues"/><category term="starlette"/></entry><entry><title>Weeknotes: Parallel SQL queries for Datasette, plus some middleware tricks</title><link href="https://simonwillison.net/2022/Apr/27/parallel-queries/#atom-tag" rel="alternate"/><published>2022-04-27T19:01:30+00:00</published><updated>2022-04-27T19:01:30+00:00</updated><id>https://simonwillison.net/2022/Apr/27/parallel-queries/#atom-tag</id><summary type="html">
    &lt;p&gt;A promising new performance optimization for Datasette, plus new &lt;code&gt;datasette-gzip&lt;/code&gt; and &lt;code&gt;datasette-total-page-time&lt;/code&gt; plugins.&lt;/p&gt;
&lt;h4&gt;Parallel SQL queries in Datasette&lt;/h4&gt;
&lt;p&gt;From the start of the project, Datasette has been built on top of Python's &lt;code&gt;asyncio&lt;/code&gt; capabilities - mainly to benefit things like &lt;a href="https://docs.datasette.io/en/stable/csv_export.html#streaming-all-records"&gt;streaming enormous CSV files&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This week I started experimenting with a new way to take advantage of them, by exploring the potential to run multiple SQL queries in parallel.&lt;/p&gt;
&lt;p&gt;Consider &lt;a href="https://latest-with-plugins.datasette.io/github/commits?_facet=repo&amp;amp;_facet=committer"&gt;this Datasette table&lt;/a&gt; page:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/sql-parallel-commits.jpg" alt="Screenshot of the commits table, showing a count and suggested facets and activated facets and some table data" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;That page has to execute quite a few SQL queries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;select count(*) ...&lt;/code&gt; to populate the 3,283 rows heading at the top&lt;/li&gt;
&lt;li&gt;Queries against each column to decide what the "suggested facets" should be (&lt;a href="https://docs.datasette.io/en/stable/facets.html#suggested-facets"&gt;details here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;For each of the selected facets (in this case &lt;code&gt;repos&lt;/code&gt; and &lt;code&gt;committer&lt;/code&gt;) a &lt;code&gt;select name, count(*) from ... group by name order by count(*) desc&lt;/code&gt; query&lt;/li&gt;
&lt;li&gt;The actual &lt;code&gt;select * from ... limit 101&lt;/code&gt; query used to display the actual table&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It ends up executing more than 30 queries! Which may seem like a lot, but &lt;a href="https://www.sqlite.org/np1queryprob.html"&gt;Many Small Queries Are Efficient In SQLite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One thing that's interesting about the above list of queries though is that they don't actually have any dependencies on each other. There's no reason not to run all of them in parallel - later queries don't depend on the results from earlier queries.&lt;/p&gt;
&lt;p&gt;I've been exploring a fancy way of executing parallel code using pytest-style dependency injection in my &lt;a href="https://github.com/simonw/asyncinject"&gt;asyncinject&lt;/a&gt; library. But I decided to do a quick prototype to see what this would look like using &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"&gt;asyncio.gather()&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It turns out that simpler approach worked surprisingly well!&lt;/p&gt;
&lt;p&gt;You can follow my research &lt;a href="https://github.com/simonw/datasette/issues/1723"&gt;in this issue&lt;/a&gt;, but the short version is that as-of a few days ago the Datasette &lt;code&gt;main&lt;/code&gt; branch runs many of the above queries in parallel.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://latest-with-plugins.datasette.io/github/commits?_facet=repo&amp;amp;_facet=committer&amp;amp;_trace=1"&gt;This trace&lt;/a&gt; (using the &lt;a href="https://datasette.io/plugins/datasette-pretty-traces"&gt;datasette-pretty-traces&lt;/a&gt; plugin) illustrates my initial results:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/sql-parallel-trace.jpg" alt="Screenshot of a trace - many SQL queries have overlapping lines" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, the grey lines for many of those SQL queries are now overlapping.&lt;/p&gt;
&lt;p&gt;You can add the undocumented &lt;code&gt;?_noparallel=1&lt;/code&gt; query string parameter to disable parallel execution to &lt;a href="https://latest-with-plugins.datasette.io/github/commits?_facet=repo&amp;amp;_facet=committer&amp;amp;_trace=1&amp;amp;_noparallel=1"&gt;compare the difference&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/sql-parallel-trace-noparallel.jpg" alt="Same trace again, but this time each query ends before the next one begins" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;One thing that gives me pause: for this particular Datasette deployment (on the cheapest available Cloud Run instance) the overall performance difference between the two is very small.&lt;/p&gt;
&lt;p&gt;I need to dig into this deeper: on my laptop I feel like I'm seeing slightly better results, but definitely not conclusively. It may be that multiple cores are not being used effectively here.&lt;/p&gt;
&lt;p&gt;Datasette runs SQL queries in a pool of threads. You might expect Python's infamous GIL (Global Interpreter Lock) to prevent these from executing across multiple cores - but I checked, and &lt;a href="https://github.com/python/cpython/blob/f348154c8f8a9c254503306c59d6779d4d09b3a9/Modules/_sqlite/cursor.c#L749-L759"&gt;the GIL is released&lt;/a&gt; in Python's C code the moment control transfers to SQLite. And since SQLite can happily run multiple threads, my hunch is that this means parallel queries should be able to take advantage of multiple cores. Theoretically at least!&lt;/p&gt;
&lt;p&gt;I haven't yet figured out how to prove this though, and I'm not currently convinced that parallel queries are providing any overall benefit at all. If you have any ideas I'd love to hear them - I have a &lt;a href="https://github.com/simonw/datasette/issues/1727"&gt;research issue&lt;/a&gt; open, comments welcome!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 28th April 2022:&lt;/strong&gt; Research continues, but it looks like there's little performance benefit from this. Current leading theory is that this is because of the GIL - while the SQLite C code releases the GIL, much of the activity involved in things like assembling &lt;code&gt;Row&lt;/code&gt; objects returned by a query still uses Python - so parallel queries still end up mostly blocked on a single core. Follow &lt;a href="https://github.com/simonw/datasette/issues/1727"&gt;the issue&lt;/a&gt; for more details. I started &lt;a href="https://sqlite.org/forum/forumpost/4a4b00e6d6bcf63e"&gt;a discussion on the SQLite Forum&lt;/a&gt; which has some interesting clues in it as well.&lt;/p&gt;
&lt;p&gt;Further update: It's definitely the GIL. I know because I tried running it against Sam Gross's &lt;a href="https://github.com/colesbury/nogil"&gt;nogil Python fork&lt;/a&gt; and the parallel version soundly beat the non-parallel version! Details &lt;a href="https://github.com/simonw/datasette/issues/1727#issuecomment-1112889800"&gt;in this comment&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="datasette-gzip"&gt;datasette-gzip&lt;/h4&gt;
&lt;p&gt;I've been putting off investigating gzip support for Datasette for a long time, because it's easy to add as a separate layer. If you run Datasette behind Cloudflare or an Apache or Nginx proxy configuring gzip can happen there, with very little effort and fantastic performance.&lt;/p&gt;
&lt;p&gt;Then I noticed that my Global Power Plants demo returned an HTML table page that weighed in at 420KB... but gzipped was just 16.61KB. Turns out HTML tables have a ton of repeated markup and compress REALLY well!&lt;/p&gt;
&lt;p&gt;More importantly: Google Cloud Run doesn't gzip for you. So all of my Datasette instances that were running on Cloud Run without also using Cloudflare were really suffering.&lt;/p&gt;
&lt;p&gt;So this morning I released &lt;a href="https://datasette.io/plugins/datasette-gzip"&gt;datasette-gzip&lt;/a&gt;, a plugin that gzips content if the browser sends an &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;The plugin is an incredibly thin wrapper around the thorougly proven-in-production &lt;a href="https://www.starlette.io/middleware/#gzipmiddleware"&gt;GZipMiddleware&lt;/a&gt;. So thin that this is &lt;a href="https://github.com/simonw/datasette-gzip/blob/0.1/datasette_gzip/__init__.py"&gt;the full implementation&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;middleware&lt;/span&gt;.&lt;span class="pl-s1"&gt;gzip&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;GZipMiddleware&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;hookimpl&lt;/span&gt;(&lt;span class="pl-s1"&gt;trylast&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;asgi_wrapper&lt;/span&gt;(&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;):
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-v"&gt;GZipMiddleware&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;This kind of thing is exactly why I &lt;a href="https://simonwillison.net/2019/Jun/23/datasette-asgi/"&gt;ported Datasette to ASGI&lt;/a&gt; back in 2019 - and why I continue to think that the burgeoning ASGI ecosystem is the most under-rated piece of today's Python web development environment.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-gzip/blob/0.1/tests/test_gzip.py"&gt;plugin's tests&lt;/a&gt; are a lot more interesting.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;@hookimpl(trylast=True)&lt;/code&gt; line is there to ensure that this plugin runs last, after ever other plugin has executed.&lt;/p&gt;
&lt;p&gt;This is necessary because there are existing ASGI plugins for Datasette (such as the new &lt;a href="https://datasette.io/plugins/datasette-total-page-time"&gt;datasette-total-page-time&lt;/a&gt;) which modify the generated request.&lt;/p&gt;
&lt;p&gt;If the gzip plugin runs before they do, they'll get back a blob of gzipped data rather than the HTML that they were expecting. This is likely to break them.&lt;/p&gt;
&lt;p&gt;I wanted to prove to myself that &lt;code&gt;trylast=True&lt;/code&gt; would prevent these errors - so I ended up writing &lt;a href="https://github.com/simonw/datasette-gzip/blob/0.1/tests/test_gzip.py#L83"&gt;a test&lt;/a&gt; that demonstrated that the plugin registered with &lt;code&gt;trylast=True&lt;/code&gt; was compatible with a transforming content plugin (in the test it just converts everything to uppercase) whereas &lt;code&gt;tryfirst=True&lt;/code&gt; would instead result in an error.&lt;/p&gt;
&lt;p&gt;Thankfully I have an older TIL on &lt;a href="https://til.simonwillison.net/pytest/registering-plugins-in-tests"&gt;Registering temporary pluggy plugins inside tests&lt;/a&gt; that I could lean on to help figure out how to do this.&lt;/p&gt;
&lt;p&gt;The plugin is now running on my &lt;a href="https://latest-with-plugins.datasette.io/global-power-plants/global-power-plants"&gt;latest-with-plugins demo instance&lt;/a&gt;. Since that instance loads dozens of different plugins it ends up serving a bunch of extra JavaScript and CSS, all of which benefits from gzip:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/datasette-gzip-results.png" alt="Screenshot of the Firefox inspector pane showing 419KB of HTML reduced to 16.61KB and 922KB of JavaScript reduced to 187.47KB. Total of 3.08MB page weight but only 546KB transferred." style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;datasette-total-page-time&lt;/h4&gt;
&lt;p&gt;To help understand the performance improvements introduced by parallel SQL queries I decided I wanted the Datasette footer to be able to show how long it took for the entire page to load.&lt;/p&gt;
&lt;p&gt;This is a tricky thing to do: how do you measure the total time for a page and then include it on that page if the page itself hasn't finished loading when you render that template?&lt;/p&gt;
&lt;p&gt;I came up with a pretty devious middleware trick to solve this, released as the &lt;a href="https://datasette.io/plugins/datasette-total-page-time"&gt;datasette-total-page-time&lt;/a&gt; plugin.&lt;/p&gt;
&lt;p&gt;The trick is to start a timer when the page load begins, and then end that timer at the very last possible moment as the page is being served back to the user.&lt;/p&gt;
&lt;p&gt;Then, inject the following HTML directly after the closing &lt;code&gt;&amp;lt;/html&amp;gt;&lt;/code&gt; tag (which works fine, even though it's technically invalid):&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;footer&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"footer"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;footer&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;let&lt;/span&gt; &lt;span class="pl-s1"&gt;ms&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;37.224&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;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;` &amp;amp;middot; Page took &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;ms&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toFixed&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;3&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;ms`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;footer&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerHTML&lt;/span&gt; &lt;span class="pl-c1"&gt;+=&lt;/span&gt; &lt;span class="pl-s1"&gt;s&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;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This adds the timing information to the page's &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; element, if one exists.&lt;/p&gt;
&lt;p&gt;You can see this running on &lt;a href="https://latest-with-plugins.datasette.io/github/commits?_trace=1"&gt;this latest-with-plugins page&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-gzip"&gt;datasette-gzip&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-gzip/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2022-04-27
&lt;br /&gt;Add gzip compression to Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-total-page-time"&gt;datasette-total-page-time&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-total-page-time/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2022-04-26
&lt;br /&gt;Add a note to the Datasette footer measuring the total page load time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/asyncinject"&gt;asyncinject&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/asyncinject/releases/tag/0.5"&gt;0.5&lt;/a&gt; - (&lt;a href="https://github.com/simonw/asyncinject/releases"&gt;7 releases total&lt;/a&gt;) - 2022-04-22
&lt;br /&gt;Run async workflows using pytest-fixtures-style dependency injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/1.1"&gt;1.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;35 releases total&lt;/a&gt;) - 2022-04-20
&lt;br /&gt;Django app for building dashboards using raw SQL queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.13"&gt;0.13&lt;/a&gt; - (&lt;a href="https://github.com/simonw/shot-scraper/releases"&gt;14 releases total&lt;/a&gt;) - 2022-04-18
&lt;br /&gt;Tools for taking automated screenshots of websites&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/sphinx/blacken-docs"&gt;Format code examples in documentation with blacken-docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/macos/open-files-with-opensnoop"&gt;Seeing files opened by a process using opensnoop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/macos/atuin"&gt;Atuin for zsh shell history in SQLite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="gzip"/><category term="python"/><category term="datasette"/><category term="weeknotes"/></entry><entry><title>gzthermal</title><link href="https://simonwillison.net/2017/Nov/21/gzthermal/#atom-tag" rel="alternate"/><published>2017-11-21T14:56:41+00:00</published><updated>2017-11-21T14:56:41+00:00</updated><id>https://simonwillison.net/2017/Nov/21/gzthermal/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://encode.ru/threads/1889-gzthermal-pseudo-thermal-view-of-Gzip-Deflate-compression-efficiency"&gt;gzthermal&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“pseudo thermal view of Gzip/Deflate compression efficiency”—neat tool for visualizing gzip compressed data and understanding exactly how run-length encoding and back references apply to a gzipped file.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://blog.usejournal.com/of-svg-minification-and-gzip-21cd26a5d007"&gt;Of SVG, Minification and Gzip&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="gzip"/></entry><entry><title>Of SVG, Minification and Gzip</title><link href="https://simonwillison.net/2017/Nov/21/svg-minification-and-gzip/#atom-tag" rel="alternate"/><published>2017-11-21T14:54:15+00:00</published><updated>2017-11-21T14:54:15+00:00</updated><id>https://simonwillison.net/2017/Nov/21/svg-minification-and-gzip/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.usejournal.com/of-svg-minification-and-gzip-21cd26a5d007"&gt;Of SVG, Minification and Gzip&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Delightfully nerdy exploration of tricks you can use to hand-optimize your SVG in order to maximize gzip compression. Premature optimization may be the root of all evil but this is still a great way to learn about how gzip actually works.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/minification"&gt;minification&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/svg"&gt;svg&lt;/a&gt;&lt;/p&gt;



</summary><category term="gzip"/><category term="minification"/><category term="svg"/></entry><entry><title>gzip support for Amazon Web Services CloudFront</title><link href="https://simonwillison.net/2010/Nov/12/gzip/#atom-tag" rel="alternate"/><published>2010-11-12T05:33:00+00:00</published><updated>2010-11-12T05:33:00+00:00</updated><id>https://simonwillison.net/2010/Nov/12/gzip/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.nomitor.com/blog/2010/11/10/gzip-support-for-amazon-web-services-cloudfront/"&gt;gzip support for Amazon Web Services CloudFront&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This would have saved me a bunch of work a few weeks ago. CloudFront can now be pointed at your own web server rather than S3, and you can ask it to forward on the Accept-Encoding header and cache multiple content versions based on the result.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cloudfront"&gt;cloudfront&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="cloudfront"/><category term="gzip"/><category term="http"/><category term="recovered"/></entry><entry><title>Velocity: Forcing Gzip Compression</title><link href="https://simonwillison.net/2010/Sep/30/gzip/#atom-tag" rel="alternate"/><published>2010-09-30T17:45:00+00:00</published><updated>2010-09-30T17:45:00+00:00</updated><id>https://simonwillison.net/2010/Sep/30/gzip/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.stevesouders.com/blog/2010/07/12/velocity-forcing-gzip-compression/"&gt;Velocity: Forcing Gzip Compression&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Almost every browser supports gzip these days, but 15% of web requests have had their Accept-Encoding header stripped or mangled, generally due to poorly implemented proxies or anti-virus software. Steve Souders passes on a trick used by Google Search, where an iframe is used to test the browser’s gzip support and set a cookie to force gzipping of future pages.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/proxies"&gt;proxies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/steve-souders"&gt;steve-souders&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="gzip"/><category term="performance"/><category term="proxies"/><category term="steve-souders"/><category term="recovered"/></entry><entry><title>PNGStore - Embedding compressed CSS &amp; JavaScript in PNGs</title><link href="https://simonwillison.net/2010/Aug/23/pngstore/#atom-tag" rel="alternate"/><published>2010-08-23T09:47:00+00:00</published><updated>2010-08-23T09:47:00+00:00</updated><id>https://simonwillison.net/2010/Aug/23/pngstore/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.iamcal.com/png-store/"&gt;PNGStore - Embedding compressed CSS &amp;amp; JavaScript in PNGs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cal did some further analysis on the CSS/JS to PNG compression trick (including producing some interesting images of jQuery compressed using different image packing techniques) and found it to be slightly less effective than regular GZipping.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cal-henderson"&gt;cal-henderson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/png"&gt;png&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="cal-henderson"/><category term="gzip"/><category term="png"/><category term="recovered"/></entry><entry><title>Paul Buchheit: Make your site faster and cheaper to operate in one easy step</title><link href="https://simonwillison.net/2009/Apr/17/paul/#atom-tag" rel="alternate"/><published>2009-04-17T17:19:44+00:00</published><updated>2009-04-17T17:19:44+00:00</updated><id>https://simonwillison.net/2009/Apr/17/paul/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://paulbuchheit.blogspot.com/2009/04/make-your-site-faster-and-cheaper-to.html"&gt;Paul Buchheit: Make your site faster and cheaper to operate in one easy step&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Paul promotes gzip encoding using nginx as a proxy, and mentions that FriendFeed use a “custom, epoll-based python server” as their application server. Does that mean that they’re serving their real-time comet feeds directly from Python?


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/comet"&gt;comet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/epoll"&gt;epoll&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/friendfeed"&gt;friendfeed&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nginx"&gt;nginx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/paul-buchheit"&gt;paul-buchheit&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="comet"/><category term="epoll"/><category term="friendfeed"/><category term="gzip"/><category term="nginx"/><category term="paul-buchheit"/><category term="python"/></entry></feed>