Full Width [alt+shift+f] FOCUS MODE Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
19
Caches are a wonderful way to make your most frequent operations cheaper. If you’ve got a resource somewhere on disk (or a network hop away) that is accessed often, changes infrequently, and fits in memory, you’ve got an excellent candidate for a cache! Caching Celebrity Posts For example, consider a social media post from a famous celebrity. This celebrity has 100,000,000 followers, around 5% of which are active at any given time (that’s 5,000,000 users). Any of those users accessing a post from our celebrity would require us to go lookup the post in our database and then serve it to the user. That’s not a huge deal if that’s all our database is doing, but if it’s busy handling other operations as well, we might want to avoid asking for the same data from it over and over again. The celebrity post never changes (or maybe very infrequently could be edited), it’s got some metadata i.e. like counts, repost counts, and reply counts which we want to be relatively fresh but the number we...
a year ago

Comments

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from exist

When Imperfect Systems are Good, Actually: Bluesky's Lossy Timelines

Often when designing systems, we aim for perfection in things like consistency of data, availability, latency, and more. The hardest part of system design is that it’s difficult (if not impossible) to design systems that have perfect consistency, perfect availability, incredibly low latency, and incredibly high throughput, all at the same time. Instead, when we approach system design, it’s best to treat each of these properties as points on different axes that we balance to find the “right fit” for the application we’re supporting. I recently made some major tradeoffs in the design of Bluesky’s Following Feed/Timeline to improve the performance of writes at the cost of consistency in a way that doesn’t negatively affect users but reduced P99s by over 96%. Timeline Fanout When you make a post on Bluesky, your post is indexed by our systems and persisted to a database where we can fetch it to hydrate and serve in API responses. Additionally, a reference to your post is “fanned out” to your followers so they can see it in their Timelines. This process involves looking up all of your followers, then inserting a new row into each of their Timeline tables in reverse chronological order with a reference to your post. When a user loads their Timeline, we fetch a page of post references and then hydrate the posts/actors concurrently to quickly build an API response and let them see the latest content from people they follow. The Timelines table is sharded by user. This means each user gets their own Timeline partition, randomly distributed among shards of our horizontally scalable database (ScyllaDB), replicated across multiple shards for high availability. Timelines are regularly trimmed when written to, keeping them near a target length and dropping older post references to conserve space. Hot Shards in Your Area Bluesky currently has around 32 Million Users and our Timelines database is broken into hundreds of shards. To support millions of partitions on such a small number of shards, each user’s Timeline partition is colocated with tens of thousands of other users’ Timelines. Under normal circumstances with all users behaving well, this doesn’t present a problem as the work of an individual Timeline is small enough that a shard can handle the work of tens of thousands of them without being heavily taxed. Unfortunately, with a large number of users, some of them will do abnormal things like… well… following hundreds of thousands of other users. Generally, this can be dealt with via policy and moderation to prevent abusive users from causing outsized load on systems, but these processes take time and can be imperfect. When a user follows hundreds of thousands of others, their Timeline becomes hyperactive with writes and trimming occurring at massively elevated rates. This load slows down the individual operations to the user’s Timeline, which is fine for the bad behaving user, but causes problems to the tens of thousands of other users sharing a shard with them. We typically call this situation a “Hot Shard”: where some resident of a shard has “hot” data that is being written to or read from at much higher rates than others. Since the data on the shard is only replicated a few times, we can’t effectively leverage the horizontal scale of our database to process all this additional work. Instead, the “Hot Shard” ends up spending so much time doing work for a single partition that operations to the colocated partitions slow down as well. Stacking Latencies Returning to our Fanout process, let’s consider the case of Fanout for a user followed by 2,000,000 other users. Under normal circumstances, writing to a single Timeline takes an average of ~600 microseconds. If we sequentially write to the Timelines of our user’s followers, we’ll be sitting around for 20 minutes at best to Fanout this post. If instead we concurrently Fanout to 1,000 Timelines at once, we can complete this Fanout job in ~1.2 seconds. That sounds great, except it oversimplifies an important property of systems: tail latencies. The average latency of a write is ~600 microseconds, but some writes take much less time and some take much more. In fact, the P99 latency of writes to the Timelines cluster can be as high as 15 milliseconds! What does this mean for our Fanout? Well, if we concurrently write to 1,000 Timelines at once, statistically we’ll see 10 writes as slow as or slower than 15 milliseconds. In the case of timelines, each “page” of followers is 10,000 users large and each “page” must be fanned out before we fetch the next page. This means that our slowest writes will hold up the fetching and Fanout of the next page. How does this affect our expected Fanout time? Each “page” will have ~100 writes as slow as or slower than the P99 latency. If we get unlucky, they could all stack up on a single routine and end up slowing down a single page of Fanout to 1.5 seconds. In the worst case, for our 2,000,000 Follower celebrity, their post Fanout could end up taking as long as 5 minutes! That’s not even considering P99.9 and P99.99 latencies which could end up being >1 second, which could leave us waiting tens of minutes for our Fanout job. Now imagine how bad this would be for a user with 20,000,000+ Followers! So, how do we fix the problem? By embracing imperfection, of course! Lossy Timelines Imagine a user who follows hundreds of thousands of others. Their Timeline is being written to hundreds of times a second, moving so fast it would be humanly impossible to keep up with the entirety of their Timeline even if it was their full-time job. For a given user, there’s a threshold beyond which it is unreasonable for them to be able to keep up with their Timeline. Beyond this point, they likely consume content through various other feeds and do not primarily use their Following Feed. Additionally, beyond this point, it is reasonable for us to not necessarily have a perfect chronology of everything posted by the many thousands of users they follow, but provide enough content that the Timeline always has something new. Note in this case I’m using the term “reasonable” to loosely convey that as a social media service, there must be a limit to the amount of work we are expected to do for a single user. What if we introduce a mechanism to reduce the correctness of a Timeline such that there is a limit to the amount of work a single Timeline can place on a DB shard. We can assert a reasonable limit for the number of follows a user should have to have a healthy and active Timeline, then increase the “lossiness” of their Timeline the further past that limit they go. A loss_factor can be defined as min(reasonable_limit/num_follows, 1) and can be used to probabilistically drop writes to a Timeline to prevent hot shards. Just before writing a page in Fanout, we can generate a random float between 0 and 1, then compare it to the loss_factor of each user in the page. If the user’s loss_factor is smaller than the generated float, we filter the user out of the page and don’t write to their Timeline. Now, users all have the same number of “follows worth” of Fanout. For example with a reasonable_limit of 2,000, a user who follows 4,000 others will have a loss_factor of 0.5 meaning half the writes to their Timeline will get dropped. For a user following 8,000 others, their loss factor of 0.25 will drop 75% of writes to their Timeline. Thus, each user has a effective ceiling on the amount of Fanout work done for their Timeline. By specifying the limits of reasonable user behavior and embracing imperfection for users who go beyond it, we can continue to provide service that meets the expectations of users without sacrificing scalability of the system. Aside on Caching We write to Timelines at a rate of more than one million times a second during the busy parts of the day. Looking up the number of follows of a given user before fanning out to them would require more than one million additional reads per second to our primary database cluster. This additional load would not be well received by our database and the additional cost wouldn’t be worth the payoff for faster Timeline Fanout. Instead, we implemented an approach that caches high-follow accounts in a Redis sorted set, then each instance of our Fanout service loads an updated version of the set into memory every 30 seconds. This allows us to perform lookups of follow counts for high-follow accounts millions of times per second per Fanount service instance. By caching values which don’t need to be perfect to function correctly in this case, we can once again embrace imperfection in the system to improve performance and scalability without compromising the function of the service. Results We implemented Lossy Timelines a few weeks ago on our production systems and saw a dramatic reduction in hot shards on the Timelines database clusters. In fact, there now appear to be no hot shards in the cluster at all, and the P99 of a page of Fanout work has been reduced by over 90%. Additionally, with the reduction in write P99s, the P99 duration for a full post Fanout has been reduced by over 96%. Jobs that used to take 5-10 minutes for large accounts now take <10 seconds. Knowing where it’s okay to be imperfect lets you trade consistency for other desirable aspects of your systems and scale ever higher. There are plenty of other places for improvement in our Timelines architecture, but this step was a big one towards improving throughput and scalability of Bluesky’s Timelines. If you’re interested in these sorts of problems and would like to help us build the core data services that power Bluesky, check out this job listing. If you’re interested in other open positions at Bluesky, you can find them here.

6 months ago 53 votes
Emoji Griddle
10 months ago 28 votes
Jetstream: Shrinking the AT Proto Firehose by >99%

Bluesky recently saw a massive spike in activity in response to Brazil’s ban of Twitter. As a result, the AT Proto event firehose provided by Bluesky’s Relay at bsky.network has increased in volume by a huge amount. The average event rate during this surge increased by ~1,300%. Before this new surge in activity, the firehose would produce around 24 GB/day of traffic. After the surge, this volume jumped to over 232 GB/day! Keeping up with the full, verified firehose quickly became less practical on cheap cloud infrastructure with metered bandwidth. To help reduce the burden of operating bots, feed generators, labelers, and other non-verifying AT Proto services, I built Jetstream as an alternative, lightweight, filterable JSON firehose for AT Proto. How the Firehose Works The AT Proto firehose is a mechanism used to keep verified, fully synced copies of the repos of all users. Since repos are represented as Merkle Search Trees, each firehose event contains an update to the user’s MST which includes all the changed blocks (nodes in the path from the root to the modified leaf). The root of this path is signed by the repo owner, and a consumer can keep their copy of the repo’s MST up-to-date by applying the diff in the event. For a more in-depth explanation of how Merkle Trees are constructed, check out this explainer. Practically, this means that for every small JSON record added to a repo, we also send along some number of MST blocks (which are content-addressed hashes and thus very information-dense) that are mostly useful for consumers attempting to keep a fully synced, verified copy of the repo. You can think of this as the difference between cloning a git repo v.s. just grabbing the latest version of the files without the .git folder. In this case, the firehose effectively streams the diffs for the repository with commits, signatures, and metadata, which is inherently heavier than a point-in-time checkout of the repo. Because firehose events with repo updates are signed by the repo owner, they allow a consumer to process events from any operator without having to trust the messenger. This is the “Authenticated” part of the Authenticated Transfer (AT) Protocol and is crucial to the correct functioning of the network. That being said, of the hundreds of consumers of Bluesky’s production Relay, >90% of them are building feeds, bots, and other tools that don’t keep full copies of the entire network and don’t verify MST operations at all. For these consumers, all they actually process is the JSON records created, updated, and deleted in each event. If consumers already trust the provider to do validation on their end, they could get by with a much more lightweight data stream. How Jetstream Works Jetstream is a streaming service that consumes an AT Proto com.atproto.sync.subscribeRepos stream and converts it into lightweight, friendly JSON. If you want to try it out yourself, you can connect to my public Jetstream instance and view all posts on Bluesky in realtime: $ websocat "wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post" Note: the above instance is operated by Bluesky PBC and is free to use, more instances are listed in the official repo Readme Jetstream converts the CBOR-encoded MST blocks produced by the AT Proto firehose and translates them into JSON objects that are easier to interface with using standard tooling available in programming languages. Since Repo MSTs only contain records in their leaf nodes, this means Jetstream can drop all of the blocks in an event except for those of the leaf nodes, typically leaving only one block per event. In reality, this means that Jetstream’s JSON firehose is nearly 1/10 the size of the full protocol firehose for the same events, but lacks the verifiability and signatures included in the protocol-level firehose. Jetstream events end up looking something like: { "did": "did:plc:eygmaihciaxprqvxpfvl6flk", "time_us": 1725911162329308, "type": "com", "commit": { "rev": "3l3qo2vutsw2b", "type": "c", "collection": "app.bsky.feed.like", "rkey": "3l3qo2vuowo2b", "record": { "$type": "app.bsky.feed.like", "createdAt": "2024-09-09T19:46:02.102Z", "subject": { "cid": "bafyreidc6sydkkbchcyg62v77wbhzvb2mvytlmsychqgwf2xojjtirmzj4", "uri": "at://did:plc:wa7b35aakoll7hugkrjtf3xf/app.bsky.feed.post/3l3pte3p2e325" } }, "cid": "bafyreidwaivazkwu67xztlmuobx35hs2lnfh3kolmgfmucldvhd3sgzcqi" } } Each event lets you know the DID of the repo it applies to, when it was seen by Jetstream (a time-based cursor), and up to one updated repo record as serialized JSON. Check out this 10 second CPU profile of Jetstream serving 200k evt/sec to a local consumer: By dropping the MST and verification overhead by consuming from relay we trust, we’ve reduced the size of a firehose of all events on the network from 232 GB/day to ~41GB/day, but we can do better. Jetstream and zstd I recently read a great engineering blog from Discord about their use of zstd to compress websocket traffic to/from their Gateway service and client applications. Since Jetstream emits marshalled JSON through the websocket for developer-friendliness, I figured it might be a neat idea to see if we could get further bandwidth reduction by employing zstd to compress events we send to consumers. zstd has two basic operating modes, “simple” mode and “streaming” mode. Streaming Compression At first glance, streaming mode seems like it’d be a great fit. We’ve got a websocket connection with a consumer and streaming mode allows the compression to get more efficient over the lifetime of the connection. I went and implemented a streaming compression version of Jetstream where a consumer can request compression when connecting and will get zstd compressed JSON sent as binary messages over the socket instead of plaintext. Unfortunately, this had a massive impact on Jetstream’s server-side CPU utilization. We were effectively compressing every message once per consumer as part of their streaming session. This was not a scalable approach to offering compression on Jetstream. Additionally, Jetstream stores a buffer of the past 24 hours (configurable) of events on disk in PebbleDB to allow consumers to replay events before getting transitioned into live-tailing mode. Jetstream stores serialized JSON in the DB, so playback is just shuffling the bytes into the websocket without having to round-trip the data into a Go struct. When we layer in streaming compression, playback becomes significantly more expensive because we have to compress outgoing events on-the-fly for a consumer that’s catching up. In real numbers, this increased CPU usage of Jetstream by 23% while lowering the throughput of playback from ~200k evt/sec to ~28k evt/sec for a single local consumer. When in streaming mode, we can’t leverage the bytes we compress for one consumer and reuse them for another consumer because zstd’s streaming context window may not be in sync between the two consumers. They haven’t received exactly the same data in the session so the clients on the other end don’t have their state machines in the same state. Since streaming mode’s primary advantage is giving us eventually better efficiency as the encoder learns about the data, what if we just taught the encoder about the data at the start and compress each message statelessly? Dictionary Mode zstd offers a mechanism for initializing an encoder/decoder with pre-optimized settings by providing a dictionary trained on a sample of the data you’ll be encoding/decoding. Using this dictionary, zstd essentially uses it’s smallest encoded representations for the most frequently seen patterns in the sample data. In our case, where we’re compressing serialized JSON with a common event shape and lots of common property names, training a dictionary on a large number of real events should allow us to represent the common elements among messages in the smallest number of bytes. For take two of Jetstream with zstd, let’s to use a single encoder for the whole service that utilizes a custom dictionary trained on 100,000 real events. We can use this encoder to compress every event as we see it, before persisting and emitting it to consumers. Now we end up with two copies of every event, one that’s just serialized JSON, and one that’s statelessly compressed to zstd using our dictionary. Any consumers that want compression can have a copy of the dictionary on their end to initialize a decoder, then when we broadcast the shared compressed event, all consumers can read it without any state or context issues. This requires the consumers and server to have a pre-shared dictionary, which is a major drawback of this implementation but good enough for our purposes. That leaves the problem of event playback for compression-enabled clients. An easy solution here is to just store the compressed events as well! Since we’re only sticking the JSON records into our PebbleDB, the actual size of the 24 hour playback window is <8GB with sstable compression. If we store a copy of the JSON serialized event and a copy of the zstd compressed event, this will, at most, double our storage requirements. Then during playback, if the consumer requests compression, we can just shuffle bytes out of the compressed version of the DB into their socket instead of having to move it through a zstd encoder. Savings Running with a custom dictionary, I was able to get the average Jetstream event down from 482 bytes to just 211 bytes (~0.44 compression ratio). Jetstream allows us to live tail all posts on Bluesky as they’re posted for as little as ~850 MB/day, and we could keep up with all events moving through the firehose during the Brazil Twitter Exodus weekend for 18GB/day (down from 232GB/day). With this scheme, Jetstream is required to compress each event only once before persisting it to disk and emitting it to connected consumers. The CPU impact of these changes is significant in proportion to Jetstream’s incredibly light load but it’s a flat cost we pay once no matter how many consumers we have. (CPU profile from a 30 second pprof sample with 12 consumers live-tailing Jetstream) Additionally, with Jetstream’s shared buffer broadcast architecture, we keep memory allocations incredibly low and the cost per consumer on CPU and RAM is trivial. In the allocation profile below, more than 80% of the allocations are used to consume the full protocol firehose. The total resident memory of Jetstream sits below 16MB, 25% of which is actually consumed by the new zstd dictionary. To bring it all home, here’s a screenshot from the dashboard of my public Jetstream instance serving 12 consumers all with various filters and compression settings, running on a $5/mo OVH VPS. At our new baseline firehose activity, a consumer of the protocol-level firehose would require downloading ~3.16TB/mo to keep up. A Jetstream consumer getting all created, updated, and deleted records without compression enabled would require downloading ~400GB/mo to keep up. A Jetstream consumer that only cares about posts and has zstd compression enabled can get by on as little as ~25.5GB/mo, <99% of the full weight firehose. Feel free to join the conversation about Jetstream and zstd on Bluesky.

11 months ago 32 votes
How HLS Works

Over the past few weeks, I’ve been building out server-side short video support for Bluesky. The major aim of this feature is to support short (90 second max) video streaming at a quality that doesn’t cost an arm and a leg for us to provide for free. In order to stay within these constraints, we’re considering making use of a video CDN that can bear the brunt of the bandwidth required to support Video-on-Demand streaming. While the CDN is a pretty fully-featured product, we want to avoid too much vendor lock-in and provide some enhancements to our streaming platform that requires extending their offering and getting creative with video streaming protocols. Some of the things we’d like to be able to do that don’t work out-of-the-box are: Track view counts, viewer sessions, and duration viewed to provide better feedback for video performance. Provide dynamic closed-caption support with the flexibility to automate them in the future. Store a transcoded version of source files somewhere durable to provide a “source of truth” for videos when needed. Append a “trailer” to the end of video streams for some branding in a TikTok-esque 3-second snippet. In this post I’ll be focusing on the HLS-related features above, namely view/duration accounting, closed captions, and trailers. HLS is Just a Bunch of Text files HTTP Live Streaming (HLS) is a standard established by Apple in 2009 that allows for adaptive-bitrate live and Video-on-Demand (VOD) streaming. For the purposes of this blog post, I’ll restrict my explanations to how HLS VOD streaming works. A player that implements the HLS protocol is capable of dynamically adjusting the quality of a streamed video based on network conditions. Additionally, a server that implements the HLS protocol should provide one or more variants of a media stream which accommodate varying network qualities to allow for graceful degradation of stream quality without stopping playback. HLS implements this by producing a series of plaintext (.m3u8) “playlist” files that tell the player what bitrates and resolutions the server provides so that the player can decide which variant it should stream. HLS differentiates between two kinds of “playlist” files: Master Playlists, and Media Playlists. Master Playlists A Master Playlist is the first file fetched by your video player. It contains a series of variants which point to child Media Playlists. It also describes the approximate bitrate of the variant sources and the codecs and resolutions used by those sources. $ curl https://my.video.host.com/video_15/playlist.m3u8 #EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=688540,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360 360p/video.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1921217,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720 720p/video.m3u8 In the above file, the key things to notice are the RESOLUTION parameters and the {res}/video.m3u8 links. Your media player will generally start with the lowest resolution version before jumping up to higher resolutions once the network speed between you and the server is dialed in. The links in this file are pointers to Media Playlists, generally as relative paths from the Master Playlist such that, if we wanted to grab the 720p Media Playlist, we’d navigate to: https://my.video.host.com/video_15/720p/video.m3u8. A Master Playlist can also contain multi-track audio directives and directives for closed-captions but for now let’s move onto the Media Playlist. Media Playlists A Media Playlist is yet another plaintext file that provides your video player with two key bits of data: a list of media Segments (encoded as .ts video files) and headers for each Segment that tell the player the runtime of the media. $ curl https://my.video.host.com/video_15/720p/video.m3u8 #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:4 #EXTINF:4.000, video0.ts #EXTINF:4.000, video1.ts #EXTINF:4.000, video2.ts #EXTINF:4.000, video3.ts #EXTINF:4.000, video4.ts #EXTINF:2.800, video5.ts This Media Playlist describes a video that’s 22.8 seconds long (5 x 4-second Segments + 1 x 2.8-second Segment). The playlist describes a VOD piece of media, meaning we know this playlist contains the entirety of the media the player needs. The TARGETDURATION tells us the maximum length of each Segment so the player knows how many Segments to buffer ahead of time. During live streaming, that also lets the player know how frequently to refresh the playlist file to discover new Segments. Finally the EXTINF headers for each Segment indicate the duration of the following .ts Segment file and the relative paths of the video#.ts tell the player where to load the actual media files from. Where’s the Actual Media? At this point, the video player has loaded two .m3u8 playlist files and got lots of metadata about how to play the video but it hasn’t actually loaded any media files. The .ts files referenced in the Media Playlist are where the real media is, so if we wanted to control the playlists but let the CDN handle serving actual media, we can just redirect those video#.ts requests to our CDN. .ts files are Transport Stream MPEG-2 encoded short media files that can contain video or audio and video. Tracking Views To track views of our HLS streams, we can leverage the fact that every video player must first load the Master Playlist. When a user requests the Master Playlist, we can modify the results dynamically to provide a SessionID to each response and allow us to track the user session without cookies or headers: #EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=688540,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360 360p/video.m3u8?session_id=12345 #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1921217,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720 720p/video.m3u8?session_id=12345 Now when their video player fetches the Media Playlists, it’ll include a query-string that we can use to identify the streaming session, ensuring we don’t double-count views on the video and can track which Segments of video were loaded in the session. #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:4 #EXTINF:4.000, video0.ts?session_id=12345&duration=4 #EXTINF:4.000, video1.ts?session_id=12345&duration=4 #EXTINF:4.000, video2.ts?session_id=12345&duration=4 #EXTINF:4.000, video3.ts?session_id=12345&duration=4 #EXTINF:4.000, video4.ts?session_id=12345&duration=4 #EXTINF:2.800, video5.ts?session_id=12345&duration=2.8 Finally when the video player fetches the media Segment files, we can measure the Segment view before we redirect to our CDN with a 302, allowing us to know the amount of video-seconds loaded in the session and which Segments were loaded. This method has limitations, namely that a media player loading a segment doesn’t necessarily mean it showed that segment to the viewer, but it’s the best we can do without an instrumented media player. Adding Subtitles Subtitles are included in the Master Playlist as a variant and then are referenced in each of the video variants to let the player know where to load subs from. #EXTM3U #EXT-X-VERSION:3 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="en_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="en",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/en.m3u8" #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=688540,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,SUBTITLES="subs" 360p/video.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1921217,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=1280x720,SUBTITLES="subs" 720p/video.m3u8 Just like with the video Media Playlists, we need a Media Playlist file for the subtitle track as well so that the player knows where to load the source files from and what duration of the stream they cover. $ curl https://my.video.host.com/video_15/subtitles/en.m3u8 #EXTM3U #EXT-X-VERSION:3 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:22.8 #EXTINF:22.800, en.vtt In this case, since we’re only serving a short video, we can just provide a single Segment that points at a WebVTT subtitle file encompassing the entire duration of the video. If you crack open the en.vtt file you’ll see something like: $ curl https://my.video.host.com/video_15/subtitles/en.vtt WEBVTT 00:00.000 --> 00:02.000 According to all known laws of aviation, 00:02.000 --> 00:04.000 there is no way a bee should be able to fly. 00:04.000 --> 00:06.000 Its wings are too small to get its fat little body off the ground. ... The media player is capable of reading WebVTT and presenting the subtitles at the right time to the viewer. For longer videos you may want to break up your VTT files into more Segments and update the subtitle Media Playlist accordingly. To provide multiple languages and versions of subtitles, just add more EXT-X-MEDIA:TYPE=SUBTITLES lines to the Master Playlist and tweak the NAME, LANGUAGE (if different), and URI of the additional subtitle variant definitions. #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="en_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="en",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/en.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="fr_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="fr",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/fr.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="ja_subtitle",DEFAULT=NO,AUTOSELECT=yes,LANGUAGE="ja",FORCED="no",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subtitles/ja.m3u8" Appending a Trailer For branding purposes (and in other applications, for advertising purposes), it can be helpful to insert Segments of video into a playlist to change the content of the video without requiring the content be appended to and re-encoded with the source file. Thankfully, HLS allows us to easily insert Segments into the Media Playlist using this one neat trick: #EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:4 #EXTINF:4.000, video0.ts #EXTINF:4.000, video1.ts #EXTINF:4.000, video2.ts #EXTINF:4.000, video3.ts #EXTINF:4.000, video4.ts #EXTINF:2.800, video5.ts #EXT-X-DISCONTINUITY #EXTINF:3.337, trailer0.ts #EXTINF:1.201, trailer1.ts #EXTINF:1.301, trailer2.ts #EXT-X-ENDLIST In this Media Playlist we use HLS’s EXT-X-DISCONTINUITY header to let the video player know that the following Segments may be in a different bitrate, resolution, and aspect-ratio than the preceding content. Once we’ve provided the discontinuity header, we can add more Segments just like normal that point at a different media source broken up into .ts files. Remember, HLS allows us to use relative or absolute paths here, so we could provide a full URL for these trailer#.ts files, or virtually route them so they can retain the path context of the currently viewed video. Note that we don’t need to provide the discontinuity header here, and we could also name the trailer files something like video{6-8}.ts if we wanted to, but for clarity and proper player behavior, it’s best to use the discontinuity header if your trailer content doesn’t match the bitrate and resolution of the other video Segments. When the video player goes to play this media, it will continue from video5.ts to trailer0.ts without missing a beat, making it appear as if the trailer is part of the original video. This approach allows us to dynamically change the contents of the trailer for all videos, heavily cache the trailer .ts Segment files for performance, and avoid having to encode the trailer onto the end of every video source file. Conclusion At the end of the day, we’ve now got a video streaming service capable of tracking views and watch session durations, dynamic closed caption support, and branded trailers to help grow the platform. HLS is not a terribly complex protocol. The vast majority of it is human-readable plaintext files and is easy to inspect in the wild to how it’s used in production. When I started this project, I knew next to nothing about the protocol but was able to download some .m3u8 files and get digging to discover how the protocol worked, then build my own implementation of a HLS server to accommodate the video streaming needs of Bluesky. To learn more about HLS, you can check out the official RFC here which describes all the features discussed above and more. I hope this post encourages you to go explore other protocols you use every day by poking at them in the wild, downloading the files your browser interprets for you, and figuring out how simple some of these apparently “complex” systems are. If you’re interested in solving problems like these, take a look at our open Job Recs. If you have any questions about HLS, Bluesky, or other distributed, @scale social media infrastructure, you can find me on Bluesky here and you can discuss this post here.

a year ago 29 votes
An entire Social Network in 1.6GB (GraphD Part 2)

In Part 1 of this series, we tried to answer the question “who do you follow who also follows user B” in Bluesky, a social network with millions of users and hundreds of millions of follow relationships. At the conclusion of the post, we’d developed an in-memory graph store for the network that uses HashMaps and HashSets to keep track of the followers of every user and the set of users they follow, allowing bidirectional lookups, intersections, unions, and other set operations for combining social graph data. I received some helpful feedback after that post where several people pointed me towards Roaring Bitmaps as a potential improvement on my implementation. They were right, Roaring Bitmaps would be an excellent fit for my Graph service, GraphD, and could also provide me with a much needed way to quickly persist and load the Graph data to and from disk on startup, hopefully reducing the startup time of the service. What are Bitmaps? If you just want to dive into the Roaring Bitmap spec, you can read the paper here, but it might be easier to first talk about bitmaps in general. You can think of a bitmap as a vector of one-bit values (like booleans) that let you encode a set of integer values. For instance, say we have 10,000 users on our website and want to keep track of which users have validated their email addresses. We could do this by creating a list of the uint32 user IDs of each user, in which case if all 10,000 users have validated their emails we’re storing 10k * 32 bits = 40KB. Or, we could create a vector of single-bit values that’s 10,000 bits long (10k / 8 = 1.25KB), then if a user has confirmed their email we can set the value at the index of their UID to 1. If we want to create a list of all the UIDs of validated accounts, we can walk the vector and record the index of each non-zero bit. If we want to check if user n has validated their email, we can do a O(1) lookup in the bitmap by loading the bit at index n and checking if it’s set. When Bitmaps get Big and Sparse Now when talking about our social network problem, we’re dealing with a few more than 10,000 UIDs. We need to keep track of 5.5M users and whether or not the user follows or is followed by any of the other 5.5M users in the network. To keep a bitmap of “People who follow User A”, we’re going to need 5.5M bits which would require (5.5M / 8) ~687KB of space. If we wanted to keep bitmaps of “People who follow User A” and “People who User A follows”, we’d need ~1.37MB of space per user using a simple bitmap, meaning we’d need 5,500,000 * 1.37MB = ~7.5 Terabytes of space! Clearly this isn’t an improvement of our strategy from Part 1, so how can we make this more efficient? One strategy for compressing the bitmap is to take consecutive runs of 0’s or 1’s (i.e. 00001110000001) in the bitmap and turn them into a number. For instance if we had an account that followed only the last 100 accounts in our social network, the first 5,499,900 indices in our bitmap would be 0’s and so we could represent the bitmap by saying: 5,499,900 0's, then 100 1's which you notice I’ve written here in a lot fewer than 687KB and a computer could encode using two uint32 values plus two bits (one indicator bit for the state of each run) for a total of 66 bits. This strategy is called Run Length Encoding (RLE) and works pretty well but has a few drawbacks: mainly if your data is randomly and heavily populated, you may not have many consecutive runs (imagine a bitset where every odd bit is set and every even bit is unset). Also lookups and evaluation of the bitset requires walking the whole bitset to figure out where the index you care about lives in the compressed format. Thankfully there’s a more clever way to compress bitmaps using a strategy called Roaring Bitmaps. A brief description of the storage strategy for Roaring Bitmaps from the official paper is as follows: We partition the range of 32-bit indexes ([0, n)) into chunks of 2^16 integers sharing the same 16 most significant digits. We use specialized containers to store their 16 least significant bits. When a chunk contains no more than 4096 integers, we use a sorted array of packed 16-bit integers. When there are more than 4096 integers, we use a 2^16-bit bitmap. Thus, we have two types of containers: an array container for sparse chunks and a bitmap container for dense chunks. The 4096 threshold insures that at the level of the containers, each integer uses no more than 16 bits. These bitmaps are designed to support both densely and sparsely distributed data and can provide high performance binary set operations (and/or/etc.) operating on the containers within two or more bitsets in parallel. For more info on how Roaring Bitmaps work and some neat diagrams, check out this excellent primer on Roaring Bitmaps by Vikram Oberoi. So, how does this help us build a better graph? GraphD, Revisited with Roaring Bitmaps Let’s get back to our GraphD Service, this time in Go instead of Rust. For each user we can keep track of a struct with two bitmaps: type FollowMap struct { followingBM *roaring.Bitmap followingLk sync.RWMutex followersBM *roaring.Bitmap followersLk sync.RWMutex } Our FollowMap gives us a Roaring Bitmap for both the set of users we follow, and the set of users who follow us. Adding a Follow to the graph just requires we set the right bits in both user’s respective maps: // Note I've removed locking code and error checks for brevity func (g *Graph) addFollow(actorUID, targetUID uint32) { actorMap, _ := g.g.Load(actorUID) actorMap.followingBM.Add(targetUID) targetMap, _ := g.g.Load(targetUID) targetMap.followersBM.Add(actorUID) } Even better if we want to compute the intersections of two sets (i.e. the people User A follows who also follow User B) we can do so in parallel: // Note I've removed locking code and error checks for brevity func (g *Graph) IntersectFollowingAndFollowers(actorUID, targetUID uint32) ([]uint32, error) { actorMap, ok := g.g.Load(actorUID) targetMap, ok := g.g.Load(targetUID) intersectMap := roaring.ParAnd(4, actorMap.followingBM, targetMap.followersBM) return intersectMap.ToArray(), nil } Storing the entire graph as Roaring Bitmaps in-memory costs us around 6.5GB of RAM and allows us to perform set intersections between moderately large sets (with hundreds of thousands of set bits) in under 500 microseconds while serving over 70k req/sec! And the best part of all? We can use Roaring’s serialization format to write these bitmaps to disk or transfer them over the network. Storing 164M Follows in 1.6GB In the original version of GraphD, on startup the service would read a CSV file with an adjacency list of the (ActorDID, TargetDID) pairs of all follows on the network. This required creating a CSV dump of the follows table, pausing writes to the follows table, then bringing up the service and waiting 5 minutes for it to read the CSV file, intern the DIDs as uint32 UIDs, and construct the in-memory graph. This process is slow, pauses writes for 5 minutes, and every time our service restarts we have to do it all over again! With Roaring Bitmaps, we’re now given an easy way to effectively serialize a version of the in-memory graph that is many times smaller than the adjacency list CSV and many times faster to load. We can serialize the entire graph into a SQLite DB on the local machine where each row in a table contains: (uid, DID, followers_bitmap, following_bitmap) Loading the entire graph from this SQLite DB can be done in around ~20 seconds: // Note I've removed locking code and error checks for brevity rows, err := g.db.Query(`SELECT uid, did, following, followers FROM actors;`) for rows.Next() { var uid uint32 var did string var followingBytes []byte var followersBytes []byte rows.Scan(&uid, &did, &followingBytes, &followersBytes) followingBM := roaring.NewBitmap() followingBM.FromBuffer(followingBytes) followersBM := roaring.NewBitmap() followersBM.FromBuffer(followersBytes) followMap := &FollowMap{ followingBM: followingBM, followersBM: followersBM, followingLk: sync.RWMutex{}, followersLk: sync.RWMutex{}, } g.g.Store(uid, followMap) g.setUID(did, uid) g.setDID(uid, did) } While the service is running, we can also keep track of the UIDs of actors who have added or removed a follow since the last time we saved the DB, allowing us to periodically flush changes to the on-disk SQLite only for bitmaps that have updated. Syncing our data every 5 seconds while tailing the production firehose takes 2ms and writes an average of only ~5MB to disk per flush. The crazy part of this is, the on-disk representation of our entire follow network is only ~1.6GB! Because we’re making use of Roaring’s compressed serialized format, we can turn the ~6.5GB of in-memory maps into 1.6GB of on-disk data. Our largest bitmap, the followers of the bsky.app account with over 876k members, becomes ~500KB as a blob stored in SQLite. So, to wrap up our exploration of Roaring Bitmaps for first-degree graph databases, we saw: A ~20% reduction in resident memory size compared to HashSets and HashMaps A ~84% reduction in the on-disk size of the graph compared to an adjacency list A ~93% reduction in startup time compared to loading from an adjacency list A ~66% increase in throughput of worst-case requests under load A ~59% reduction in p99 latency of worst-case requests under low My next iteration on this problem will likely be to make use of DGraph’s in-memory Serialized Roaring Bitmap library that allows you to operate on fully-compressed bitmaps so there’s no need to serialize and deserialize them when reading from or writing to disk. It also probably results in significant memory savings as well! If you’re interested in solving problems like these, take a look at our open Backend Developer Job Rec. You can find me on Bluesky here, you can chat about this post here.

a year ago 32 votes

More in AI

Pluralistic: A weekend's worth of links (30 Aug 2025)

Today's links A weekend's worth of links: Short hits for a long weekend. Object permanence: Floppy disk CD sleeves; Rules for radicals; California's preventable fires; Muppet Haunted Mansion; Wells Fargo steals rescued Nazi loot; Texas abortion release. Upcoming appearances: Where to find me. Recent appearances: Where I've been. Latest books: You keep readin' em, I'll keep writin' 'em. Upcoming books: Like I said, I'll keep writin' 'em. Colophon: All the rest. A weekend's worth of links (permalink) Did you know that it's possible to cut a hole in any cube such that an identical cube can fit inside it? Really! It's called "Rupert's Property." Further, all Platonic solids are Rupert! Except one, newly discovered shape, which cannot fit inside itself. What is this eldritch polygon called? A Nopeterhedron! https://arxiv.org/pdf/2508.18475 "Nopeterhedron" is the best coinage I've heard in months, which makes it a natural to open this week's linkdump, a collection of the links that piled up this week without making it into my newsletter. This is my 33d Saturday linkdump – here's the previous 31 editions: https://pluralistic.net/tag/linkdump/ Speaking of eldritch geometry? Perhaps you've heard that Donald Trump plans to add a 90,000 sqft ballroom to the (55,000 sqft) White House. As Kate "McMansion Hell" Wagner writes for The Nation, this is a totally bullshit story floated by Trump and a notorious reactionary starchitect, and to call it a "plan" is to do unforgiveable violence to the noble art of planning: https://www.thenation.com/article/culture/white-house-ballroom-mccrery-postmodernism/ Wagner is both my favorite architecture critic and the only architecture critic I read. That's because she's every bit as talented a writer as she is a perspicacious architecture critic. What's more, she's a versatile writer. She doesn't just write these sober-but-scathing, erudite pieces for The Nation; she has, for many years, invented the genre of snarky Zillow annotations, which are convulsively funny and trenchant: https://mcmansionhell.com/ At the Electronic Frontier Foundation, we often find ourselves at the center of in big political legal fights; for example, we were the first group to sue Musk and DOGE: https://www.eff.org/press/releases/eff-sues-opm-doge-and-musk-endangering-privacy-millions Knowing that I'm part of this stuff helps me get through tough times – but I'm also so glad that we get to step in and defend brilliant writers like Wagner, as we did a few years ago, when Zillow tried to use legal bullying tactics to make her stop being mean to their shitty houses: https://www.eff.org/deeplinks/2017/06/mcmansion-hell-responds-zillows-unfounded-legal-claims If this kind of stuff excites you as much as it excites me and you're in the Bay Area, get thee to the EFF Awards (or tune into the livestream) and watch us honor this year's winners: Just Futures Law, Erie Meyer, and the Software Freedom Law Center, India: https://www.eff.org/deeplinks/2025/08/join-your-fellow-digital-rights-supporters-eff-awards-september-10 So much of the activity that EFF defends involves writing. The web was written into existence, after all, both by the coders who hacked it together and the writers who filled it up. I've always wanted to be a writer, since I was six years old, and I'm so lucky to have grown up through an era is which the significance of the written word has continuously expanded. I was equally lucky to have writing teachers who permanently, profoundly shaped my relationship with the written word. I've had many of those, but none were so foundational as Harriet Wolff, the longest-serving English teacher at Toronto's first alternative school, SEED School, whence I graduated after a mere seven years of instruction. Harriet was a big part of why I spent seven years getting a four year diploma. She was such a brilliant English teacher, and presided over such an excellent writing workshop, that I felt like I still had so much to learn from high school, even after I'd amassed enough credits to graduate, so I just stuck around. Harriet died this summer: https://obituaries.thestar.com/obituary/harriet-wolff-1093038534 We hadn't spoken much over the past decade, though she did come to my wedding and was every bit as charming and wonderful as I'd remembered her. Despite not having spoken to her in many years, hardly a day went by without my thinking of her and the many lessons she imparted to me. Harriet took a very broad view of what could be good writing. Though she wasn't much of a science fiction fan, she always took my sf stories seriously – as seriously as she took the more "literary" fiction and poetry submitted by my peers. She kept a filing cabinet full of mimeographs and photocopies, each excellent examples of various forms of writing. Over the years, she handed me everything from Joan Didion essays to especially sharp op-eds from Time Magazine, along with tons of fiction. Harriet taught me how to criticize fiction, as a means of improving my understand of what I was doing with my writing, and as a way of exposing other writers to new ways of squeezing their own big, numinous, irreducible feelings out of their fingertips and out onto the page. She was the first person I called when I sold my first story, at 17, and I still remember standing on the lawn of my parents' house, cordless phone in one hand and acceptance letter in the other, and basking in her approval. Harriet was a tough critiquer. Like many of the writers in her workshop, I had what you might call "glibness privilege" – a facility with words that I could use to paper over poor characterization or plotting. Whenever I'd do this, she'd fix me with her stare and say, "Cory, this is merely clever." I have used that phrase countless times – both in relation to my own work and into the work of my students. Though Harriet was unsparing in her critiques, they never stung, because she always treated the writers in her workshop as her peers in a lifelong journey to improve our craft. She'd come out for cigarettes with us, and she came to every house party I invited her to, bringing a good, inexpensive bottle of wine and finding a sofa to sit on and discuss writing an literature. She invited me to Christmas dinner one year when I was alone for the holidays and introduced me to Yorkshire pudding, still one of my favorite dishes (though none has ever matched the pleasure of eating that first one from her oven). Harriet apparently told her family that she didn't want a memorial, though from emails with her former students, I know that there might end up being something planned in Toronto. After all, memorials are for the living as much as for the dead. It's unlikely I'll be home for that one, but of course, the best way to memorialize Harriet is in writing. For Harriet, writing was a big, big church, and every kind of writing was worth serious attention. I always thought of the web as a very Wolffian innovation, because it exposed so many kinds of audiences to so many kinds of writers. There's Kate Wagner's acerbic Zillow annotations, of course, but also so much more. One of the web writers I've followed since the start is Kevin Kelly, who went from The Whole Earth Review to serving as Wired's first executive editor. Over the years, Kevin has blazed new trails for those of us who write in public, publishing many seminal pieces online. But Kevin was and is a print guy, who has blazed new trails in self-publishing, producing books that are both brilliant and beautifully wrought artifacts, like his giant, three-volume set of photos of "Vanishing Asia": https://vanishing.asia/the-making-of-vanishing-asia/ This week, Kelly published one of his famous soup-to-nuts guides to a subject: "Everything I Know about Self-Publishing": https://kk.org/thetechnium/everything-i-know-about-self-publishing/ It's a long, thoughtful, and extremely practical guide that is full of advice on everything from printing to promo. I've self-published several volumes, and I learned a lot. One very important writer who's trying something new this summer – to wonderful effect – is Hilary J Allen, a business law professor at American University. During the first cryptocurrency bubble, Allen wrote some of the sharpest critiques of fintech, dubbing it "Shadow Banking 2.0": https://pluralistic.net/2022/03/02/shadow-banking-2-point-oh/#leverage Allen also coined the term "driverless finance," a devastatingly apt description of the crypto bro's desire for a financial system with no governance, which she expounded upon in a critical book: https://driverlessfinancebook.com/ This summer, Allen has serialized "FinTech Dystopia," which she called "A summer beach read about Silicon Valley ruining things." Chapter 9 dropped this week, "Let’s Get Skeptical": https://fintechdystopia.com/chapters/chapter9.html It's a tremendous read, and while it mostly concerns itself with summarizing her arguments against the claims of fintech boosters, there's an absolutely jaw-dropped section on Neom, the doomed Saudi megaproject to build a massive "linear city" in the desert: More than 21,000 workers (primarily from India, Bangladesh, and Nepal) are reported to have died working on NEOM and related projects in Saudi Arabia since 2017, with more than 20,000 indigenous people reported to have been forcibly displaced to make room for the development. Allen offers these statistics as part of her critique of the "Abundance agenda," which focuses on overregulation as the main impediment to a better world. Like Allen, I'm not afraid to criticize bad regulation, but also like Allen, I'm keenly aware of the terrible harms that arise out of a totally unregulated system. The same goes for technology, of course. There's plenty of ways to use technology that is harmful, wasteful and/or cruel, but that isn't a brief against technology itself There are many ways that technology has been used (and can be used) to make things better. One of the pioneers of technology for good is Jim Fruchterman, founder of the venerable tech nonprofit Benetech, for which he was awarded a Macarthur "Genius" award. Fruchterman has just published his first book, with MIT Press, in which he sums up a lifetime's experience in finding ways to improve the world with technology. Appropriately enough, it's called Technology For Good: https://mitpress.mit.edu/9780262050975/technology-for-good/ After all, technology is so marvelously flexible that there's always a countertechnology, for every abusive tech. Every 10-foot digital wall implies an 11-foot digital ladder. Last month, I wrote about Echelon, a company that makes digitally connected exercise bikes, who had pushed a mandatory update to their customers' bikes that took away functionality they got for free and sold it back to them in inferior form: https://pluralistic.net/2025/07/26/manifolds/#bark-chicken-bark Repair hero Louis Rossman – who is running a new, direct action right to repair group named Fulu – offered a $20,000 bounty to anyone who could crack the firmware on an Echelon bike and create a disenshittified software stack that restored the original functionality: https://www.youtube.com/watch?v=2zayHD4kfcA In short order, app engineer Ricky Witherspoon, had cracked it, and had a way to continue to use SyncSpin, his popular app for Echelon bikes, which had been shut out by Echelon's enshittification. However, as Witherspoon told 404 Media's Jason Koebler, he won't release his code, not even for a $20,000 bounty, because doing so would make him liable to a $500,000 fine, and a five-year prison sentence, under Section 1201 of the Digital Millennium Copyright Act: https://www.404media.co/developer-unlocks-newly-enshittified-echelon-exercise-bikes-but-cant-legally-release-his-software/ Fulu paid Witherspoon anyway (they're good eggs). Witherspoon told Koebler: For now it’s just about spreading awareness that this is possible, and that there’s another example of egregious behavior from a company like this […] if one day releasing this was made legal, I would absolutely open source this. I can legally talk about how I did this to a certain degree, and if someone else wants to do this, they can open source it if they want to. Free/open source software is a powerful tonic against enshittification, and it has the alchemical property of transforming the products of bad companies into good utilities that everyone benefits from. One example of this is Whisper, an open source audio transcription model released by Openai. Since Whisper's release, free software hackers have made steady – even remarkable – improvements to it. I discovered Whisper earlier this summer, when I couldn't locate a quote I'd heard on a recent podcast that I wanted to reference in a column. I installed Whisper on my laptop and fed it the last 30+ hours' worth of podcasts I'd listened to. An hour later, it had fully transcribed all of them, with timecode, and had put so little load on my laptop that the fan didn't even turn on. I was able to search all that text, locate the quote, and use the timecode to find the clip and check the transcription. Whisper has turned extremely accurate transcription into a utility, something that can just be added to any program or operating system for free. I think this is going to be quietly revolutionary, bringing full-text search and captioning to audio and video as something we can just take for granted. That's already happening! FFMpeg is the gold-standard free software tool for converting, encoding and re-encoding video, and now the latest version integrates Whisper, allowing FFMpeg to subtitle your videos on the fly: https://www.theregister.com/2025/08/28/ffmpeg_8_huffman/ Whisper is an example of the "residue" that will be left behind when the AI bubble pops. All bubbles pop, after all, but not all bubbles leave behind a useful residue. When crypto dies, its residue will be a few programmers who've developed secure coding habits in Rust, but besides that, all that will be left behind is terrible Austrian economics and worse monkey JPEGs: https://pluralistic.net/2023/12/19/bubblenomics/#pop But the free/open source code generated by stupid and/or evil projects often lives on long after those projects are forgotten. And lots (most) of free/open code is written for good purposes. Take Madeline, a platform for tracking loans made by co-operatives, produced by the Seed Commons, which is now used by financial co-ops around the world, as they make "non-extractive investments in worker and community-owned businesses on the ground": https://seedcommons.org/posts/digital-infrastructure-for-a-non-extractive-economy-the-story-of-madeline Madeline (and Seed Commons) are one of those bright lights that are easy to miss in these brutal and terrifying times. And if that's not enough, there's always booze. If you're thinking of drowning your sorrows, you could do worse than to pour your brown liquor out of a decanter shaped like a giant Atari CX-10 joystick: https://atari.com/products/atari-joystick-decanter-set That's the kind of brand necrophilia that could really enhance a night's drinking. Object permanence (permalink) #20yrago 5.25″ floppies make great CD sleeves https://web.archive.org/web/20050924144644/http://www.readymademag.com/feature_18_monkey.php #20yrsago Hollywood can break down any door in Delhi https://web.archive.org/web/20050903065949/https://www.eff.org/deeplinks/archives/003943.php #20yrsago Side-band attack tips virtual Blackjack dealer’s hand https://web.archive.org/web/20051119111417/https://haacked.com/archive/2005/08/29/9748.aspx #20yrsago Judge to RIAA: Keep your “conference center” out of my court https://web.archive.org/web/20051001031307/http://www.godwinslaw.org/weblog/archive/2005/08/29/runaround-suits #15yrsago Which ebook sellers will allow publishers and writers to opt out of DRM? https://www.publishersweekly.com/pw/by-topic/columns-and-blogs/cory-doctorow/article/44012-doctorow-s-first-law.html #15yrsago 10 Rules for Radicals: Lessons from rogue archivist Carl Malamud https://public.resource.org/rules/ #15yrsago Homeowners’ associations: hives of petty authoritarianism https://web.archive.org/web/20100606170504/http://theweek.com/article/index/104150/top-7-insane-homeowners-association-rules #15yrsago Lynd Ward’s wordless, Depression-era woodcut novels https://memex.craphound.com/2010/08/29/lynd-wards-wordless-depression-era-woodcut-novels/#5yrsago #10yrago Suit: Wells Fargo sent contractors to break into our house, loot family treasures rescued from Nazis https://theintercept.com/2015/08/28/wells-fargo-contractors-stole-family-heirlooms/ #10yrsago Texas doctor’s consent form for women seeking abortions https://memex.craphound.com/wp-content/uploads/2020/09/3kscWU5-2-scaled.jpg #10yrsago Spear phishers with suspected ties to Russian government spoof fake EFF domain, attack White House https://www.eff.org/deeplinks/2015/08/new-spear-phishing-campaign-pretends-be-eff #10yrsago Rowlf the dog gives a dramatic reading of “Grim Grinning Ghosts.” https://www.youtube.com/watch?v=CPMTEJ_IAAU #5yrsago California's preventable fires https://pluralistic.net/2020/08/29/chickenized-home-to-roost/#cal-burning Upcoming appearances (permalink) Ithaca: AD White keynote (Cornell), Sep 12 https://deanoffaculty.cornell.edu/events/keynote-cory-doctorow-professor-at-large/ DC: Enshittification at Politics and Prose, Oct 8 https://politics-prose.com/cory-doctorow-10825 NYC: Enshittification with Lina Khan (Brooklyn Public Library), Oct 9 https://www.bklynlibrary.org/calendar/cory-doctorow-discusses-central-library-dweck-20251009-0700pm New Orleans: DeepSouthCon63, Oct 10-12 http://www.contraflowscifi.org/ Chicago: Enshittification with Anand Giridharadas (Chicago Humanities), Oct 15 https://www.oldtownschool.org/concerts/2025/10-15-2025-kara-swisher-and-cory-doctorow-on-enshittification/ San Francisco: Enshittification at Public Works (The Booksmith), Oct 20 https://app.gopassage.com/events/doctorow25 Miami: Enshittification at Books & Books, Nov 5 https://www.eventbrite.com/e/an-evening-with-cory-doctorow-tickets-1504647263469 Recent appearances (permalink) Cory Doctorow DESTROYS Enshittification (QAA Podcast) https://soundcloud.com/qanonanonymous/cory-doctorow-destroys-enshitification-e338 Divesting from Amazon’s Audible and the Fight for Digital Rights (Libro.fm) https://pocketcasts.com/podcasts/9349e8d0-a87f-013a-d8af-0acc26574db2/00e6cbcf-7f27-4589-a11e-93e4ab59c04b The Utopias Podcast https://www.buzzsprout.com/2272465/episodes/17650124 Latest books (permalink) "Picks and Shovels": a sequel to "Red Team Blues," about the heroic era of the PC, Tor Books (US), Head of Zeus (UK), February 2025 (https://us.macmillan.com/books/9781250865908/picksandshovels). "The Bezzle": a sequel to "Red Team Blues," about prison-tech and other grifts, Tor Books (US), Head of Zeus (UK), February 2024 (the-bezzle.org). "The Lost Cause:" a solarpunk novel of hope in the climate emergency, Tor Books (US), Head of Zeus (UK), November 2023 (http://lost-cause.org). "The Internet Con": A nonfiction book about interoperability and Big Tech (Verso) September 2023 (http://seizethemeansofcomputation.org). Signed copies at Book Soup (https://www.booksoup.com/book/9781804291245). "Red Team Blues": "A grabby, compulsive thriller that will leave you knowing more about how the world works than you did before." Tor Books http://redteamblues.com. "Chokepoint Capitalism: How to Beat Big Tech, Tame Big Content, and Get Artists Paid, with Rebecca Giblin", on how to unrig the markets for creative labor, Beacon Press/Scribe 2022 https://chokepointcapitalism.com Upcoming books (permalink) "Canny Valley": A limited edition collection of the collages I create for Pluralistic, self-published, September 2025 "Enshittification: Why Everything Suddenly Got Worse and What to Do About It," Farrar, Straus, Giroux, October 7 2025 https://us.macmillan.com/books/9780374619329/enshittification/ "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026 "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026 "The Memex Method," Farrar, Straus, Giroux, 2026 "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026 Colophon (permalink) Today's top sources: Currently writing: "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. (747 words yesterday, 46239 words total). FIRST DRAFT COMPLETE A Little Brother short story about DIY insulin PLANNING This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net. https://creativecommons.org/licenses/by/4.0/ Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution. How to get Pluralistic: Blog (no ads, tracking, or data-collection): Pluralistic.net Newsletter (no ads, tracking, or data-collection): https://pluralistic.net/plura-list Mastodon (no ads, tracking, or data-collection): https://mamot.fr/@pluralistic Medium (no ads, paywalled): https://doctorow.medium.com/ Twitter (mass-scale, unrestricted, third-party surveillance and advertising): https://twitter.com/doctorow Tumblr (mass-scale, unrestricted, third-party surveillance and advertising): https://mostlysignssomeportents.tumblr.com/tagged/pluralistic "When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer. ISSN: 3066-764X

2 days ago 5 votes
Tradeoffs Exist

And Denying That Has Corroded Public Discourse

2 days ago 7 votes
AI Roundup 133: Nano banana

August 29, 2025.

3 days ago 10 votes
Mass Intelligence

From GPT-5 to nano banana: everyone is getting access to powerful AI

4 days ago 12 votes
Pluralistic: The capitalism of fools (28 Aug 2025)

Today's links The capitalism of fools: Trump's mirror-world New Deal. Hey look at this: Delights to delectate. Object permanence: IBM's fabric design; Nixon Cthulu; Surveillance capitalism is capitalism, with surveillance; Dismaland ad; Outdoor ed vs TB; Mathematicians' fave chalk. Upcoming appearances: Where to find me. Recent appearances: Where I've been. Latest books: You keep readin' em, I'll keep writin' 'em. Upcoming books: Like I said, I'll keep writin' 'em. Colophon: All the rest. The capitalism of fools (permalink) As Trump rails against free trade, demands public ownership stakes in corporations that receive government funds, and (selectively) enforces antitrust law, some (stupid) people are wondering, "Is Trump a communist?" In The American Prospect, David Dayen writes about the strange case of Trump's policies, which fly in the face of right wing economic orthodoxy and have the superficial trappings of a leftist economic program: https://prospect.org/economy/2025-08-28-judge-actually-existing-trump-economy/ The problem isn't that tariffs are always bad, nor is it that demanding state ownership stakes in structurally important companies that depend on public funds is bad policy. The problem is that Trump's version of these policies sucks, because everything Trump touches dies, and because he governs solely on vibes, half-remembered wisdom imparted by the last person who spoke to him, and the dying phantoms of old memories as they vanish beneath a thick bark of amyloid plaque. Take Trump's demand for a 10% stake in Intel (a course of action endorsed by no less than Bernie Sanders). Intel is a company in trouble, whose financialization has left it dependent on other companies (notably TMSC) to make its most advanced chips. The company has hollowed itself out, jettisoning both manufacturing capacity and cash reserves, pissing away the funds thus freed up on stock buybacks and dividends. Handing Trump a 10% "golden share" does nothing to improve Intel's serious structural problems. And if you take Trump at his word and accept that securing US access to advanced chips is a national security priority, Trump's Intel plan does nothing to advance that access. But it gets worse: Trump also says denying China access to these chips is a national security priority, but he greenlit Nvidia's plan to sell its top-of-the-range silicon to China in exchange for a gaudy statuette and a 15% export tax. It's possible to pursue chip manufacturing as a matter of national industrial policy, and it's even possible to achieve this goal by taking ownership stakes in key firms – because it's often easier to demand corporate change via a board seat than it is to win the court battles needed to successfully invoke the Defense Production Act. The problem is that Trumpland is uninterested in making any of that happen. They just want a smash and grab and some red meat for the base: "Look, we made Intel squeal!" Then there's the Trump tariffs. Writing in Vox EU, Lausanne prof of international business Richard Baldwin writes about the long and checkered history of using tariffs to incubate and nurture domestic production: https://www.nakedcapitalism.com/2025/08/trumpian-tariffs-rerun-the-failed-strategy-of-import-substitution-industrialization.html The theory of tariffs goes like this: if we make imports more expensive by imposing a tax on them (tariffs are taxes that are paid by consumers, after all), then domestic manufacturers will build factories and start manufacturing the foreign goods we've just raised prices on. This is called "import substitution," and it really has worked, but only in a few cases. What do those cases have in common? They were part of a comprehensive program of "export discipline, state-directed credit, and careful government–business coordination": https://academic.oup.com/book/10201 In other words, tariffs only work to reshore production where there is a lot of careful planning, diligent data-collection, and review. Governments have to provide credit to key firms to get them capitalized, provide incentives, and smack nonperformers around. Basically, this is the stuff that Biden did for renewables with the energy sector, and – to a lesser extent – for silicon with the CHIPS Act. Trump's not doing any of that. He's just winging it. There's zero follow-through. It's all about appearances, soundbites, and the libidinal satisfaction of watching corporate titans bend the knee to your cult leader. This is also how Trump approaches antitrust. When it comes to corporate power, both Trump and Biden's antitrust enforcers are able to strike terror into the hearts of corporate behemoths. The difference is that the Biden administration prioritized monopolists based on how harmful they were to the American people and the American economy, whereas Trump's trustbusters target companies based on whether Trump is mad at them: https://pluralistic.net/2024/11/12/the-enemy-of-your-enemy/#is-your-enemy What's more, any company willing to hand a million or two to a top Trump enforcer can just walk away from the charges: https://prospect.org/power/2025-08-19-doj-insider-blows-whistle-pay-to-play-antitrust-corruption/ In her 2023 book Doppelganger, Naomi Klein introduces the idea of a right-wing "mirror world" that offers a conspiratorial, unhinged version of actual problems that leftists wrestle with: https://pluralistic.net/2023/09/05/not-that-naomi/#if-the-naomi-be-klein-youre-doing-just-fine For example, the antivax movement claims that pharma companies operate on the basis of unchecked greed, without regard to the harm their defective products cause to everyday people. When they talk about this, they sound an awful like leftists who are angry that the Sacklers killed a million Americans with their opiods and then walked away with billions of dollars: https://pluralistic.net/2023/12/05/third-party-nonconsensual-releases/#au-recherche-du-pedos-perdue Then there are the conspiracy theories about voting machines. Progressives have been sounding the alarm about the security defects in voting machine since the Bush v Gore years, but that doesn't mean that Venezuelan hackers stole the 2020 election for Biden: https://pluralistic.net/2021/01/11/seeing-things/#ess When anti-15-minute-city weirdos warn that automated license-plate cameras are a gift to tyrants both petty and gross, they are repeating a warning that leftists have sounded since the Patriot Act: https://locusmag.com/2023/05/commentary-cory-doctorow-the-swivel-eyed-loons-have-a-point/ The mirror-world is a world where real problems (the rampant sexual abuse of children by powerful people and authortiy figures) are met with fake solutions (shooting up pizza parlors and transferring Ghislaine Maxwell to a country-club prison): https://www.bbc.com/news/articles/czd049y2qymo Most of the people stuck in the mirror world are poor and powerless, because desperation makes you an easy mark for grifters peddling conspiracy theories. But Trump's policies on corporate power are what happens in the mirror world inhabited by the rich and powerful. Trump is risking the economic future of every person in America (except a few cronies), but that's not the only risk here. There's also the risk that reasonable people will come to view industrial policy, government stakes in publicly supported companies, and antitrust as reckless showboating, a tactic exclusively belonging to right wing nutjobs and would-be dictators. Sociologists have a name for this: they call it "schismogenesis," when a group defines itself in opposition to its rivals. Schismogenesis is progressives insisting that voting machines and pharma companies are trustworthy and that James Comey is a resistance hero: https://pluralistic.net/2021/12/18/schizmogenesis/ After we get rid of Trump, America will be in tatters. We're going to need big, muscular state action to revive the nation and rebuild its economy. We can't afford to let Trump poison the well for the very idea of state intervention in corporate activity. Hey look at this (permalink) Thinking Ahead to the Full Military Takeover of Cities https://www.hamiltonnolan.com/p/thinking-ahead-to-the-full-military Framework is working on a giant haptic touchpad, Trackpoint nub, and eGPU for its laptops https://www.theverge.com/news/766161/framework-egpu-haptic-touchpad-trackpoint-nub National says "fuck you" on the right to repair https://norightturn.blogspot.com/2025/08/national-says-fuck-you-on-right-to.html?m=1 Tax the Rich. They’ll Stay https://www.rollingstone.com/politics/political-commentary/zohran-mamdani-tax-rich-new-york-city-1235414327/ Welcome to the Free Online Tax Preparation Feedback Survey https://irsresearch.gov1.qualtrics.com/jfe/form/SV_ewDJ6DeBj3ockGa Object permanence (permalink) #20yrsago Cops have to pay $41k for stopping man from videoing them https://web.archive.org/web/20050905015507/http://www.paed.uscourts.gov/documents/opinions/05D0847P.pdf #20yrsago Commercial music in podcasts: the end of free expression? https://memex.craphound.com/2005/08/26/commercial-music-in-podcasts-the-end-of-free-expression/ #10yrsago North Dakota cops can now use lobbyist-approved taser/pepper-spray drones https://www.thedailybeast.com/first-state-legalizes-taser-drones-for-cops-thanks-to-a-lobbyist/ #10yrsago Illinois mayor appoints failed censor to town library board https://ncac.org/news/blog/mayor-appoints-would-be-censor-to-library-board #10yrsago IBM’s lost, glorious fabric design https://collection.cooperhewitt.org/users/mepelman/visits/qtxg/87597377/ #10yrsago Former mayor of SLC suing NSA for warrantless Olympic surveillance https://www.techdirt.com/2015/08/26/prominent-salt-lake-city-residents-sue-nsa-over-mass-warrantless-surveillance-during-2002-olympics/ #10yrsago Health’s unkillable urban legend: “You must drink 8 glasses of water/day” https://www.nytimes.com/2015/08/25/upshot/no-you-do-not-have-to-drink-8-glasses-of-water-a-day.html?_r=0 #10yrsago Austin Grossman’s CROOKED: the awful, cthulhoid truth about Richard Nixon https://memex.craphound.com/2015/08/26/austin-grossmans-crooked-the-awful-cthulhoid-truth-about-richard-nixon/ #10yrsago After Katrina, FBI prioritized cellphone surveillance https://www.muckrock.com/news/archives/2015/aug/27/stingray-katrina/ #10yrsago Germany’s spy agency gave the NSA the private data of German citizens in exchange for Xkeyscore access https://www.zeit.de/digital/datenschutz/2015-08/xkeyscore-nsa-domestic-intelligence-agency #10yrsago Elaborate spear-phishing attempt against global Iranian and free speech activists, including an EFF staffer https://citizenlab.ca/2015/08/iran_two_factor_phishing/ #10yrsago Commercial for Banksy’s Dismaland https://www.youtube.com/watch?v=V2NG-MgHqEk #5yrsago Outdoor education beat TB in 1907 https://pluralistic.net/2020/08/27/cult-chalk/#tb #5yrsago Hagoromo, mathematicians' cult chalk https://pluralistic.net/2020/08/27/cult-chalk/#hagoromo #5yrsago Principles for platform regulation https://pluralistic.net/2020/08/27/cult-chalk/#eff-eu #5yrsago It's blursday https://pluralistic.net/2020/08/26/destroy-surveillance-capitalism/#blursday #5yrsago Surveillance Capitalism is just capitalism, plus surveillance https://pluralistic.net/2020/08/26/destroy-surveillance-capitalism/#surveillance-monopolism Upcoming appearances (permalink) Ithaca: AD White keynote (Cornell), Sep 12 https://deanoffaculty.cornell.edu/events/keynote-cory-doctorow-professor-at-large/ DC: Enshittification at Politics and Prose, Oct 8 https://politics-prose.com/cory-doctorow-10825 New Orleans: DeepSouthCon63, Oct 10-12 http://www.contraflowscifi.org/ Chicago: Enshittification with Kara Swisher (Chicago Humanities), Oct 15 https://www.oldtownschool.org/concerts/2025/10-15-2025-kara-swisher-and-cory-doctorow-on-enshittification/ San Francisco: Enshittification at Public Works (The Booksmith), Oct 20 https://app.gopassage.com/events/doctorow25 Miami: Enshittification at Books & Books, Nov 5 https://www.eventbrite.com/e/an-evening-with-cory-doctorow-tickets-1504647263469 Recent appearances (permalink) Divesting from Amazon’s Audible and the Fight for Digital Rights (Libro.fm) https://pocketcasts.com/podcasts/9349e8d0-a87f-013a-d8af-0acc26574db2/00e6cbcf-7f27-4589-a11e-93e4ab59c04b The Utopias Podcast https://www.buzzsprout.com/2272465/episodes/17650124 Tariffs vs IP Law (Firewalls Don't Stop Dragons) https://www.youtube.com/watch?v=LFABFe-5-uQ Latest books (permalink) "Picks and Shovels": a sequel to "Red Team Blues," about the heroic era of the PC, Tor Books (US), Head of Zeus (UK), February 2025 (https://us.macmillan.com/books/9781250865908/picksandshovels). "The Bezzle": a sequel to "Red Team Blues," about prison-tech and other grifts, Tor Books (US), Head of Zeus (UK), February 2024 (the-bezzle.org). "The Lost Cause:" a solarpunk novel of hope in the climate emergency, Tor Books (US), Head of Zeus (UK), November 2023 (http://lost-cause.org). "The Internet Con": A nonfiction book about interoperability and Big Tech (Verso) September 2023 (http://seizethemeansofcomputation.org). Signed copies at Book Soup (https://www.booksoup.com/book/9781804291245). "Red Team Blues": "A grabby, compulsive thriller that will leave you knowing more about how the world works than you did before." Tor Books http://redteamblues.com. "Chokepoint Capitalism: How to Beat Big Tech, Tame Big Content, and Get Artists Paid, with Rebecca Giblin", on how to unrig the markets for creative labor, Beacon Press/Scribe 2022 https://chokepointcapitalism.com Upcoming books (permalink) "Canny Valley": A limited edition collection of the collages I create for Pluralistic, self-published, September 2025 "Enshittification: Why Everything Suddenly Got Worse and What to Do About It," Farrar, Straus, Giroux, October 7 2025 https://us.macmillan.com/books/9780374619329/enshittification/ "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026 "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026 "The Memex Method," Farrar, Straus, Giroux, 2026 "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026 Colophon (permalink) Today's top sources: Currently writing: "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. (1090 words yesterday, 45491 words total). A Little Brother short story about DIY insulin PLANNING This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net. https://creativecommons.org/licenses/by/4.0/ Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution. How to get Pluralistic: Blog (no ads, tracking, or data-collection): Pluralistic.net Newsletter (no ads, tracking, or data-collection): https://pluralistic.net/plura-list Mastodon (no ads, tracking, or data-collection): https://mamot.fr/@pluralistic Medium (no ads, paywalled): https://doctorow.medium.com/ Twitter (mass-scale, unrestricted, third-party surveillance and advertising): https://twitter.com/doctorow Tumblr (mass-scale, unrestricted, third-party surveillance and advertising): https://mostlysignssomeportents.tumblr.com/tagged/pluralistic "When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer. ISSN: 3066-764X

4 days ago 7 votes