<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vincent Tran</title>
    <description>The latest articles on DEV Community by Vincent Tran (@0xgosu).</description>
    <link>https://dev.to/0xgosu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2435529%2F3d8e6fd3-a6ef-41e2-bcf7-dbc8d04d17e1.jpeg</url>
      <title>DEV Community: Vincent Tran</title>
      <link>https://dev.to/0xgosu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xgosu"/>
    <language>en</language>
    <item>
      <title>Cloud in a Bottle: A Personal Cloud That Works Like an App Platform</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Mon, 07 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/cloud-in-a-bottle-a-personal-cloud-that-works-like-an-app-platform-1o14</link>
      <guid>https://dev.to/0xgosu/cloud-in-a-bottle-a-personal-cloud-that-works-like-an-app-platform-1o14</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgf6wb502oit4mmhzg7vf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgf6wb502oit4mmhzg7vf.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Self-hosting has a strange product problem. The people most motivated to own their software and data are often asked to become part-time infrastructure engineers first.&lt;/p&gt;

&lt;p&gt;Installing an application can mean choosing a reverse proxy, arranging DNS, obtaining certificates, translating a Docker Compose file, deciding where state lives, setting up backups, and maintaining yet another login. None of those jobs is impossible. Together, they turn “run this useful tool” into a weekend project with a permanent pager attached.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloudinabottle.org/blog/launch-post" rel="noopener noreferrer"&gt;Cloud in a Bottle&lt;/a&gt; is an open-source attempt to change that bargain. Its goal is not merely to put a friendly dashboard in front of containers. It treats one personally controlled server as an application platform: one identity boundary, one deployment contract, one routing layer, explicit permissions between apps, and storage whose durability is visible to both users and developers.&lt;/p&gt;

&lt;p&gt;The useful question is not whether it makes servers disappear. It cannot. The question is whether it moves routine infrastructure decisions into a small, inspectable platform without taking ownership away from the person running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cloud experience is the feature
&lt;/h2&gt;

&lt;p&gt;Cloud applications won because they removed friction. They open from any device, update without installers, share URLs naturally, and hide the machine doing the work. Centralization was not the only possible way to get those benefits, but it became the dominant business model because a vendor-operated service could pay the infrastructure bill and control the whole experience.&lt;/p&gt;

&lt;p&gt;Traditional self-hosting reverses the ownership model but often loses the product experience. Each application arrives with a different installation method and a different idea of configuration, networking, accounts, updates, and backups. The operator becomes the integration layer.&lt;/p&gt;

&lt;p&gt;Cloud in a Bottle tries to make the server itself that integration layer. It runs on an Ubuntu machine you control: spare hardware at home, a virtual private server, or a managed machine provisioned by Imbue and handed over with your SSH access. The same open-source stack runs in each case. Applications come from Git repositories and run as OCI containers, while the platform provides the shared machinery around them.&lt;/p&gt;

&lt;p&gt;That distinction matters. A container dashboard answers, “How can I start this workload?” An app platform must also answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does a human reach it securely?&lt;/li&gt;
&lt;li&gt;Which identity does the app see?&lt;/li&gt;
&lt;li&gt;What data may it access?&lt;/li&gt;
&lt;li&gt;How does it call another app without receiving all of that app’s files?&lt;/li&gt;
&lt;li&gt;Which state is backed up, and which state is disposable?&lt;/li&gt;
&lt;li&gt;What happens when the app asks for a dangerous capability?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is interesting because those answers form a coherent contract instead of a pile of setup recipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  One service controls the machine
&lt;/h2&gt;

&lt;p&gt;The architecture is intentionally small. According to the &lt;a href="https://cloudinabottle.org/docs/how_it_works/overview.html" rel="noopener noreferrer"&gt;platform overview&lt;/a&gt;, one Python service called the router faces the rest of the system. It serves the dashboard and API, builds and starts app containers, chooses the destination for incoming requests, checks authentication, and supervises the supporting network processes.&lt;/p&gt;

&lt;p&gt;Caddy terminates TLS. CoreDNS can serve the instance’s delegated DNS zone. A narrow system agent performs operations that the unprivileged service user cannot perform itself. Each installed app gets one rootless Podman container.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcloud-in-a-bottle%2Frequest-path.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcloud-in-a-bottle%2Frequest-path.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The public route narrows through one policy point before it reaches a loopback-only app container.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Consider a visit to &lt;code&gt;notes.example.com&lt;/code&gt;. A wildcard DNS record resolves the app subdomain to the server. Caddy accepts the HTTPS connection using a certificate covering the zone and its wildcard. It forwards the request over loopback to the router on port 8080. The router reads the hostname, maps &lt;code&gt;notes&lt;/code&gt; to an installed app, checks the owner’s session unless that path is public, and proxies the request to the app’s HTTP port.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cloudinabottle.org/docs/how_it_works/routing.html" rel="noopener noreferrer"&gt;routing documentation&lt;/a&gt; makes two choices that keep this manageable. First, one wildcard record covers new app names, so installing an app does not require another visit to a DNS control panel. Second, one wildcard certificate covers the zone and app subdomains. In the normal public setup, DNS-01 validation lets the instance create the temporary challenge record itself.&lt;/p&gt;

&lt;p&gt;Caddy is used for the part it handles well: TLS termination and the HTTP-to-HTTPS redirect. Bottle’s router manages the wildcard certificate workflow and generates Caddy’s configuration, while application identity and routing policy remain in the router.&lt;/p&gt;

&lt;p&gt;This is not a distributed control plane. That is a feature at this scale. One personal server does not need a miniature Kubernetes architecture. It needs a short path from hostname to process and an operator who can still understand the failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repository becomes an installable app
&lt;/h2&gt;

&lt;p&gt;An application is packaged with a Dockerfile and a &lt;code&gt;cloudinabottle.toml&lt;/code&gt; file at the repository root. The basic contract is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[app]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"reading-list"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Save and annotate articles"&lt;/span&gt;

&lt;span class="nn"&gt;[runtime.container]&lt;/span&gt;
&lt;span class="py"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Dockerfile"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;

&lt;span class="nn"&gt;[routing]&lt;/span&gt;
&lt;span class="py"&gt;public_paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/webhook"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[resources]&lt;/span&gt;
&lt;span class="py"&gt;memory_mb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
&lt;span class="py"&gt;cpu_cores&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;

&lt;span class="nn"&gt;[data]&lt;/span&gt;
&lt;span class="py"&gt;sqlite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;app_temp_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router clones the repository, reads the manifest, builds the image with rootless Podman, and starts it. The app listens on &lt;code&gt;0.0.0.0&lt;/code&gt; inside the container; the platform publishes its HTTP port only on the host loopback interface and exposes it through the router. The &lt;a href="https://cloudinabottle.org/docs/creating_an_app/overview.html" rel="noopener noreferrer"&gt;app-author guide&lt;/a&gt; also defines injected environment variables for the app name, stable app identity, service token, router URL, zone domain, and mounted data paths.&lt;/p&gt;

&lt;p&gt;This format does two jobs. It automates deployment, but it also makes privilege visible. A reviewer can see that an app wants 256 MB of memory, one SQLite database, scratch space, and a public webhook. More sensitive requests—extra Linux capabilities, host devices, direct ports, or access to every app’s data—appear in the same manifest.&lt;/p&gt;

&lt;p&gt;The platform rejects capabilities and device paths outside rootless-safe allowlists. That does not make every allowed request harmless. It does make the permission surface concrete before installation, which is far better than discovering a privileged mount deep inside an opaque Compose stack.&lt;/p&gt;

&lt;p&gt;Existing software does not have to adopt every platform feature. If it already runs in a container and speaks HTTP, a thin manifest may be enough. Native integration is optional: apps can gradually use the owner identity, declared storage, or cross-app services when those features improve the experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rootless containers reduce the default blast radius
&lt;/h2&gt;

&lt;p&gt;Cloud in a Bottle assumes that a personal server will run code the owner did not write. Its &lt;a href="https://cloudinabottle.org/docs/how_it_works/security.html" rel="noopener noreferrer"&gt;security model&lt;/a&gt; starts by placing every app in a rootless Podman container under an unprivileged host account.&lt;/p&gt;

&lt;p&gt;In rootless mode, container root maps into an unprivileged user namespace rather than to real host root. Podman’s own &lt;a href="https://docs.podman.io/en/stable/markdown/podman.1.html#rootless-mode" rel="noopener noreferrer"&gt;rootless-mode documentation&lt;/a&gt; explains that these namespaces rely on subordinate UID and GID ranges. Bottle adds idmapped bind mounts so files written as root inside a container appear on disk as files owned by the service user.&lt;/p&gt;

&lt;p&gt;The default boundary is useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app receives its own data directories, not a mount of the whole server.&lt;/li&gt;
&lt;li&gt;The router database, session state, and TLS keys are not mounted into app containers.&lt;/li&gt;
&lt;li&gt;The primary HTTP port binds to loopback, so public requests must cross router policy.&lt;/li&gt;
&lt;li&gt;The container receives the standard capability baseline plus &lt;code&gt;no-new-privileges&lt;/code&gt;, not arbitrary host powers.&lt;/li&gt;
&lt;li&gt;Client-supplied &lt;code&gt;X-OpenHost-*&lt;/code&gt; headers are stripped before trusted identity headers are added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are containment layers, not a proof of safety. The project itself is explicit that catalog review is not a guarantee. App authors can update code after inclusion, elevated manifest permissions widen the boundary, and a vulnerability in the container runtime or the router could still matter.&lt;/p&gt;

&lt;p&gt;That honesty is important. “Sandboxed” should mean that the default blast radius is smaller and privileges are reviewable. It should not mean that untrusted code has become risk-free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication belongs at the front door
&lt;/h2&gt;

&lt;p&gt;By default, every route in every app requires the owner’s session. Authentication happens in the router before the request reaches the container. One domain-scoped session cookie covers the dashboard and app subdomains, which gives the system the feel of a single device rather than a folder of unrelated services.&lt;/p&gt;

&lt;p&gt;Apps can declare public path prefixes for webhooks, shared pages, or their own login flows. On those routes, the app becomes responsible for access decisions. If a public route is visited by the owner, the router adds &lt;code&gt;X-OpenHost-Is-Owner: true&lt;/code&gt;, allowing an app to expose a public view and still reveal editing controls to the authenticated owner.&lt;/p&gt;

&lt;p&gt;The platform has separate credentials for separate actors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;th&gt;Principal&lt;/th&gt;
&lt;th&gt;Main use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session cookie&lt;/td&gt;
&lt;td&gt;Human owner in a browser&lt;/td&gt;
&lt;td&gt;Dashboard and protected app routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API token&lt;/td&gt;
&lt;td&gt;Owner-operated CLI, script, or agent&lt;/td&gt;
&lt;td&gt;Management API calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App token&lt;/td&gt;
&lt;td&gt;One installed application&lt;/td&gt;
&lt;td&gt;Permissioned cross-app service calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The separation prevents an app token from becoming a dashboard credential. The router also rejects owner sessions on cross-origin requests, reducing the chance that JavaScript in one app can silently act as the owner against another app.&lt;/p&gt;

&lt;p&gt;Central authentication is convenient, but it also concentrates responsibility. The router is now a security boundary for every application. Session handling, host matching, proxy headers, cross-origin checks, and public-path matching deserve more scrutiny than they would in a simple launcher. The trade is sensible only if the shared boundary is kept small enough to inspect and test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apps cooperate through grants, not shared disks
&lt;/h2&gt;

&lt;p&gt;Containerizing every app creates another problem: useful software often needs to cooperate. A mail summarizer may need a mail provider. A photo tool may need a library. A coding agent may need secrets without receiving the owner’s entire password store.&lt;/p&gt;

&lt;p&gt;Cloud in a Bottle addresses this with &lt;a href="https://cloudinabottle.org/docs/creating_an_app/cross_app_services.html" rel="noopener noreferrer"&gt;cross-app services&lt;/a&gt;. A provider declares a versioned HTTP service. A consumer declares the service version it needs, a short local name, and the grants it requests. The owner approves those grants during installation. Calls then pass through the router using the consumer’s injected app token.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcloud-in-a-bottle%2Fcross-app-permissions.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcloud-in-a-bottle%2Fcross-app-permissions.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The router authenticates and carries the grant; the provider still decides what that grant permits.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Service identity is a URL plus a semantic version. Consumers can request a compatible range; the router resolves an installed provider for each call. The current built-in examples include secrets and OAuth services, but the mechanism is general.&lt;/p&gt;

&lt;p&gt;There is a subtle division of responsibility. The router authenticates the caller, selects a provider, and forwards only the grants the owner approved. The provider enforces the meaning of those grants. A mail provider, for example, might define scopes for a mailbox or folder and supply its own approval UI for data-dependent choices.&lt;/p&gt;

&lt;p&gt;This is more work than mounting a shared directory, but it preserves boundaries. The consumer receives a narrow operation instead of ambient access to another app’s state. It also lets multiple implementations satisfy the same service contract, which is the beginning of a real personal-cloud ecosystem rather than a hard-coded bundle.&lt;/p&gt;

&lt;p&gt;The design is still young. Service URLs are currently used mainly as comparable identifiers, and the documentation anticipates richer machine-readable specifications later. Version negotiation and provider-defined permissions will need discipline as the catalog grows. Still, the direction is sound: integration should be an explicit capability, not an accidental consequence of co-location.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage is split by semantics
&lt;/h2&gt;

&lt;p&gt;Many self-hosted deployments treat storage as one volume and leave every application to invent its own backup story. Bottle exposes three tiers with different contracts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Backing&lt;/th&gt;
&lt;th&gt;Backup expectation&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;td&gt;Local disk&lt;/td&gt;
&lt;td&gt;Included in normal backups&lt;/td&gt;
&lt;td&gt;SQLite, configuration, indexes, notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary&lt;/td&gt;
&lt;td&gt;Local scratch&lt;/td&gt;
&lt;td&gt;Recreatable, not guaranteed&lt;/td&gt;
&lt;td&gt;Thumbnails, transcodes, build artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive&lt;/td&gt;
&lt;td&gt;JuiceFS over local disk or S3&lt;/td&gt;
&lt;td&gt;Managed separately from normal backups&lt;/td&gt;
&lt;td&gt;Photos, video, attachments, model weights&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://cloudinabottle.org/docs/how_it_works/data.html" rel="noopener noreferrer"&gt;data guide&lt;/a&gt; draws an important technical line. Embedded databases need local storage with real &lt;code&gt;fsync&lt;/code&gt;, locks, and strict filesystem behavior. They belong in permanent storage. The archive tier is network-shaped when backed by S3; it is appropriate for whole objects, not SQLite write-ahead logs or correctness-sensitive locks.&lt;/p&gt;

&lt;p&gt;A media application might therefore keep metadata and its search index on permanent local disk while placing original videos in archive storage. Temporary transcodes can be discarded and rebuilt. The manifest communicates that intent to the platform and to the operator.&lt;/p&gt;

&lt;p&gt;The abstraction also supports migration. Archive storage is always presented through JuiceFS, whether its objects live locally or in S3. Changing the backing store can preserve the application’s path layout. That portability is valuable, but it does not erase physics: uncached object reads can be much slower, and an archive stored only on the same local disk is not magically backed up.&lt;/p&gt;

&lt;p&gt;Bottle includes a storage guard that stops apps when free disk space falls below a configured reserve, 500 MB by default. Stopping early is disruptive; filling the disk that contains databases, logs, and control-plane state is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The remaining work is operational, not conceptual
&lt;/h2&gt;

&lt;p&gt;Cloud in a Bottle removes many repetitive choices, but ownership still has obligations. A serious deployment needs a domain or a clear local-network plan, reachable ports, security updates, backups that have been restored in practice, and a recovery path if the only server fails.&lt;/p&gt;

&lt;p&gt;Home hosting adds NAT, dynamic addresses, power loss, and residential-network constraints. A VPS simplifies reachability but returns hardware trust and a monthly bill to a provider. Managed hosting reduces setup but still requires the owner to understand where data lives and who can access the machine. The three deployment modes trade convenience, control, and operational burden; none wins every axis.&lt;/p&gt;

&lt;p&gt;The project is also early. Its launch post says the core was built and privately tested for more than six months, while the site describes active development and a deliberately small curated catalog. Early adopters should expect rough edges, inspect requested permissions, pin or review application sources when risk matters, and avoid treating catalog inclusion as a security audit.&lt;/p&gt;

&lt;p&gt;A practical evaluation can stay small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start on a disposable VPS or virtual machine rather than the only copy of important data.&lt;/li&gt;
&lt;li&gt;Install one simple HTTP app with no elevated capabilities.&lt;/li&gt;
&lt;li&gt;Verify that protected routes reject unauthenticated requests and intended public paths remain reachable.&lt;/li&gt;
&lt;li&gt;Locate the app’s permanent, temporary, and archive directories on disk.&lt;/li&gt;
&lt;li&gt;Run a backup, destroy the test app or instance, and prove that restoration works.&lt;/li&gt;
&lt;li&gt;Review how an update changes the app manifest before applying it.&lt;/li&gt;
&lt;li&gt;Only then move a useful but recoverable workload onto the platform.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence tests the ownership claim, not only the happy-path installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right abstraction is a small personal platform
&lt;/h2&gt;

&lt;p&gt;The strongest idea in Cloud in a Bottle is not one-click deployment. It is the decision to make personal infrastructure behave like a platform without pretending it is a hyperscale cloud.&lt;/p&gt;

&lt;p&gt;The router creates a single front door. Rootless containers establish useful defaults. The manifest makes resources and privilege requests visible. Storage tiers force applications to distinguish durable state from scratch data and bulk objects. Cross-app services make cooperation permissioned instead of ambient. A shared login turns separate web tools into something closer to one owned device.&lt;/p&gt;

&lt;p&gt;None of those mechanisms is individually novel. Their value comes from the product boundary they form together. If the implementation remains compact, the permissions remain legible, and recovery stays first-class, a personal cloud can be more than a hobbyist dashboard. It can become a credible place to run the growing amount of useful software that should not require surrendering the data, the machine, or the exit door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloudinabottle.org/blog/launch-post" rel="noopener noreferrer"&gt;Cloud in a Bottle launch post&lt;/a&gt;, the project’s motivation and initial scope.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudinabottle.org/docs/" rel="noopener noreferrer"&gt;Cloud in a Bottle manual&lt;/a&gt;, owner and app-author documentation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudinabottle.org/docs/how_it_works/overview.html" rel="noopener noreferrer"&gt;Platform overview&lt;/a&gt;, the router, network path, credentials, and on-disk layout.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudinabottle.org/docs/creating_an_app/manifest_spec.html" rel="noopener noreferrer"&gt;App manifest specification&lt;/a&gt;, deployment, resource, routing, and permission fields.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudinabottle.org/docs/how_it_works/security.html" rel="noopener noreferrer"&gt;Security model&lt;/a&gt;, the documented guarantees and trust boundary.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cloud-in-a-bottle/cloud-in-a-bottle" rel="noopener noreferrer"&gt;Cloud in a Bottle source&lt;/a&gt;, the AGPL-3.0 implementation.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Stop Paying Frontier-Model Prices for Agent I/O</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/stop-paying-frontier-model-prices-for-agent-io-3kdi</link>
      <guid>https://dev.to/0xgosu/stop-paying-frontier-model-prices-for-agent-io-3kdi</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91lupzndj0hwd5is473j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91lupzndj0hwd5is473j.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The expensive part of an AI coding session is not always the difficult part. A frontier model may spend most of its context reading four thousand lines to locate one pattern, then spend premium output tokens reproducing a test structure that already exists twenty times in the repository. The judgment is valuable. The transport is not.&lt;/p&gt;

&lt;p&gt;Spotify’s Portal team built a small experiment around that mismatch. Its &lt;code&gt;shunt&lt;/code&gt; plugin keeps Claude Code as the coordinator, but diverts two predictable kinds of work to cheaper, narrowly instructed workers: reading large files and producing pattern-based boilerplate. The published benchmark reports 82–94% fewer coordinator tokens for large-file reading, with a mean reduction of 90% across three read scenarios.&lt;/p&gt;

&lt;p&gt;The headline is attractive, but the architecture is more useful than the number. It combines a hard routing policy, tiny command-line adapters, reusable agent definitions, and a clear boundary around what must remain with the stronger model. This is not a general claim that small models can replace frontier models. It is a design for preventing the frontier model from doing work that never needed its judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context is a working set, not a storage system
&lt;/h2&gt;

&lt;p&gt;A coding agent needs enough evidence to decide what to do. It does not need every byte involved in gathering that evidence.&lt;/p&gt;

&lt;p&gt;When an agent reads a large source file directly, the complete tool result enters the conversation. Those tokens can help with the immediate question, but they also occupy the working context used by later turns. Read a source file, its tests, two interfaces, and a configuration file, and the session may carry a large amount of text whose only purpose was to establish a few facts.&lt;/p&gt;

&lt;p&gt;That creates three costs. The obvious one is billed input. The second is attention: relevant details must compete with old file contents on subsequent turns. The third is compaction. Once the conversation approaches its context limit, the client has to summarize or discard earlier material, which can erase precisely the decisions the developer wanted to preserve.&lt;/p&gt;

&lt;p&gt;The better unit to retain is often a compact observation: which class owns the behavior, which methods touch the database, which tests establish the convention, and where the relevant section begins. The raw corpus can stay outside the coordinator’s context unless an edit requires exact text.&lt;/p&gt;

&lt;p&gt;That is the key separation in shunt. The cheaper worker consumes the bulk input. The coordinator consumes the answer.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fportal-shunt-model-routing%2Frouting-architecture.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fportal-shunt-model-routing%2Frouting-architecture.svg%25E2%2580%259D" alt="“Architecture" width="800" height="400"&gt;&lt;/a&gt;The coordinator keeps the goal and judgment. Worker modes absorb high-volume input and output, then return only the evidence the coordinator needs.   &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Portal modes turn workers into configuration
&lt;/h2&gt;

&lt;p&gt;The worker side is built with &lt;a href="https://backstage.spotify.com/docs/portal/core-features-and-plugins/aika/modes" rel="noopener noreferrer"&gt;AiKA modes&lt;/a&gt;. A mode is a declarative agent definition: a name, instructions, optional model choice, resource limits, visibility, and an optional set of MCP tools. Spotify’s documentation also supports processors for planning, answer formatting, verification, confidence scoring, and context management.&lt;/p&gt;

&lt;p&gt;That makes a mode closer to a versioned operational role than an open-ended chat. The &lt;code&gt;bulk-reader&lt;/code&gt; role is told to read supplied files, answer one question, emit concise structured bullets, and avoid everything the caller did not ask for. The &lt;code&gt;code-writer&lt;/code&gt; role receives a specification plus a required reference file and is told to match the reference’s conventions while returning code without explanation.&lt;/p&gt;

&lt;p&gt;Narrow instructions matter because delegation only saves coordinator tokens when the result is smaller and cleaner than the source material. A worker that writes a friendly preamble, repeats the task, wraps code in fences, and appends a long explanation pushes avoidable text back across the boundary. Output discipline is part of the interface contract.&lt;/p&gt;

&lt;p&gt;Modes also separate the routing decision from the worker implementation. The plugin can continue calling &lt;code&gt;bulk-reader&lt;/code&gt; while an administrator changes the underlying model, temperature, prompt, or attached tools. Public modes can be shared across a workspace, while a user’s private mode with the same name takes precedence during server-side resolution. Teams can therefore publish a sensible default without preventing local specialization.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/spotify/portal-ai-plugins" rel="noopener noreferrer"&gt;Portal AI Plugins repository&lt;/a&gt; exposes these workflows through the Portal CLI actions registry. The invocation is ephemeral: each call stands alone, and the worker does not preserve the conversation server-side. Re-sending a file on a follow-up still costs worker tokens, but it does not refill the coordinator’s more valuable context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers make routing reliable
&lt;/h2&gt;

&lt;p&gt;Putting “use a cheaper model for large files” in a project instruction file is a suggestion. It may work until the agent decides that a direct read looks easier. Shunt makes the important part enforceable with three layers.&lt;/p&gt;

&lt;p&gt;The first layer is a pair of &lt;code&gt;PreToolUse&lt;/code&gt; hooks. Before a full &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;check-file-size&lt;/code&gt; counts the target’s lines. A file over the configured threshold—350 lines by default—is blocked, and the tool result directs Claude to the bulk-reader skill. Targeted reads with an offset or limit still pass because the agent has already narrowed the evidence it needs.&lt;/p&gt;

&lt;p&gt;The second hook inspects shell commands. It catches broad uses of &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;less&lt;/code&gt;, and &lt;code&gt;more&lt;/code&gt; against large files, but allows targeted pipelines and redirections. This prevents the simple bypass where the agent stops using its read tool and streams the same file through a shell command.&lt;/p&gt;

&lt;p&gt;The second layer is transport. The &lt;code&gt;bulk-read&lt;/code&gt; and &lt;code&gt;code-write&lt;/code&gt; scripts accept named arguments, build the request, invoke the Portal action, unwrap errors, and clean the response. Files are wrapped in XML elements carrying their paths so boundaries remain unambiguous. The agent calls a stable command instead of improvising JSON and shell quoting on every delegation.&lt;/p&gt;

&lt;p&gt;The third layer is guidance. Skills describe when the commands apply and show their exact syntax. Guidance alone is soft; hooks alone can only say no. Together, the skill presents the preferred path when the hook closes the expensive one.&lt;/p&gt;

&lt;p&gt;This combination is a general pattern for agent systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put non-negotiable cost or safety rules in deterministic code.&lt;/li&gt;
&lt;li&gt;Hide transport details behind a small, testable interface.&lt;/li&gt;
&lt;li&gt;Teach the model how and when to use that interface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each layer has one job. The hook decides whether the attempted operation is allowed. The script performs the integration. The skill supplies semantic judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threshold is an economic boundary
&lt;/h2&gt;

&lt;p&gt;Delegation is not free. It adds serialization, a network round trip, queueing, another model call, and a response that the coordinator must inspect. For a 90-line file, direct reading may be both faster and cheaper. For a 7,000-line source-and-test pair, paying that fixed overhead can be a good trade.&lt;/p&gt;

&lt;p&gt;The line threshold is therefore not merely a guardrail; it is a crude cost model. It approximates the point where the expected tokens avoided exceed the cost and latency of another invocation.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fportal-shunt-model-routing%2Frouting-decision.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fportal-shunt-model-routing%2Frouting-decision.svg%25E2%2580%259D" alt="“Flowchart" width="800" height="400"&gt;&lt;/a&gt;Route by task shape, not by a belief that one model should handle everything. Small, exact, or judgment-heavy work stays local.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;A production threshold should be measured rather than copied. File length is only a proxy for token count. Minified code, generated schemas, prose, and sparse source files have very different token densities. Cached prompts can change input economics. Worker pricing and latency change by provider. A team should record at least source tokens, returned tokens, wall time, failure rate, and how often the coordinator has to reopen the original file.&lt;/p&gt;

&lt;p&gt;An adaptive router could use those observations to choose a path by estimated tokens instead of lines. It might raise the threshold when the worker is slow, lower it for generated files, or bypass delegation when a cached prefix makes a direct read cheap. The current 350-line rule has a different virtue: developers can predict it, explain it, and debug it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why boilerplate is a separate path
&lt;/h2&gt;

&lt;p&gt;Large reads make input expensive. Boilerplate makes output expensive.&lt;/p&gt;

&lt;p&gt;For a routine test file, the coordinator may first read an existing test, then generate hundreds of lines that repeat its fixtures, naming, assertions, and cleanup. Shunt’s &lt;code&gt;code-write&lt;/code&gt; path sends a specification and reference file to the worker and can write the result directly to disk. The coordinator does not need to consume its own generated file as conversational output.&lt;/p&gt;

&lt;p&gt;The required reference is the most important constraint. “Write tests for UserService” invites generic code. “Write tests for UserService using OrderServiceTest as the reference” provides a local grammar: framework, imports, fixture lifecycle, naming, assertion style, and failure conventions. The worker performs constrained imitation rather than free-form design.&lt;/p&gt;

&lt;p&gt;This path is intentionally less enforceable than bulk reading. The plugin can reliably detect an oversized file before a read, but it cannot know from a prompt alone whether a requested implementation is boilerplate or architecture. &lt;code&gt;code-writer&lt;/code&gt; therefore depends on the coordinator recognizing a predictable task. That is a sensible limit. False-positive delegation of a delicate implementation would cost more than the output tokens it saved.&lt;/p&gt;

&lt;p&gt;Direct-to-disk output also changes verification. Saving context is not permission to trust the file. The coordinator should inspect the diff, run formatting and tests, and bring any surprising section back into its own context. Delegation moves production; it does not move accountability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep reasoning where the evidence converges
&lt;/h2&gt;

&lt;p&gt;The original experiment names four categories that should remain with the frontier model: debugging, editing, small-file work, and architectural decisions.&lt;/p&gt;

&lt;p&gt;Debugging is difficult to summarize before the cause is known. A cheap reader may report surface patterns while omitting the interaction that creates a race, leak, or invalid state transition. Architecture is similar: the work is choosing between competing constraints, not extracting facts from a file.&lt;/p&gt;

&lt;p&gt;Editing requires exact local evidence. A summary can identify the relevant method, but a patch needs the actual signature, nearby control flow, formatting, and current line content. Shunt handles this by allowing targeted reads. The worker performs reconnaissance; the coordinator opens the discovered region and makes the change.&lt;/p&gt;

&lt;p&gt;The right mental model is not “smart model versus cheap model.” It is evidence compression followed by judgment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workers scan, classify, extract, translate, and reproduce known patterns.&lt;/li&gt;
&lt;li&gt;The coordinator sets the goal, resolves ambiguity, reasons across evidence, edits sensitive code, and verifies the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That division also limits failure impact. A worker receives a bounded corpus and one narrow question. It does not need the full conversation, broad repository permissions, or every tool attached to the main agent. The &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/architecture" rel="noopener noreferrer"&gt;Model Context Protocol architecture&lt;/a&gt; follows a related separation: hosts coordinate context and security while focused servers expose specific capabilities. In both cases, narrower interfaces make composition easier to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the benchmark as a routing result
&lt;/h2&gt;

&lt;p&gt;The repository’s benchmark covers a 162,000-line Java monorepo. A 4,014-line file falls from 33,684 coordinator tokens to 5,737. A 7,408-line source-and-test pair falls from 75,990 to 4,148. A 1,281-line cross-service task falls from 16,221 to 821. Those reductions are 82%, 94%, and 94%, producing the reported 90% mean for bulk reads.&lt;/p&gt;

&lt;p&gt;These are project measurements published with the plugin, not an independent benchmark or a universal discount. They show that the chosen tasks had a high compression ratio: a worker could turn a large corpus into a small useful answer. Results will change with repository language, question complexity, model choice, response rules, caching, and how often the coordinator must request exact follow-up context.&lt;/p&gt;

&lt;p&gt;The more durable metric is useful coordinator tokens per completed task. A routing system has failed if it reports a 95% token reduction but causes repeated calls, worse patches, missed defects, or long idle waits. Cost, latency, and correctness have to be measured together.&lt;/p&gt;

&lt;p&gt;Spotify’s plugin includes hook, transport, and end-to-end evaluations in addition to the benchmark. That is the right direction. Routing policy is production code. A small shell parsing change can create an escape hatch, block a legitimate targeted command, or send too much data through an operating system’s argument-size limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a router from the boundary inward
&lt;/h2&gt;

&lt;p&gt;The useful way to adopt this pattern is to begin with one task that has all four properties: frequent, high-volume, easy to specify, and easy to verify. Large-file question answering is a strong first candidate. Translation, documentation normalization, generated configuration, and conventional tests may follow.&lt;/p&gt;

&lt;p&gt;For each route, define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The trigger: size, command shape, file type, or explicit skill call.&lt;/li&gt;
&lt;li&gt;The worker contract: exact inputs, concise output form, and refusal conditions.&lt;/li&gt;
&lt;li&gt;The escape hatch: targeted reads or an explicit coordinator override.&lt;/li&gt;
&lt;li&gt;The verifier: tests, schema validation, diff review, or a stronger model pass.&lt;/li&gt;
&lt;li&gt;The telemetry: tokens, time, retries, reopen rate, and accepted-result rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then make the boundary visible. If a hook blocks an operation, its message should explain why and provide the next command. If a worker times out, the coordinator should know whether to split the request, retry, or perform the task directly. Silent routing is difficult to trust because developers cannot connect a delay or quality change to its cause.&lt;/p&gt;

&lt;p&gt;Finally, treat all worker output as untrusted input. A delegated summary can be wrong, and a delegated code file can compile while violating a hidden invariant. Give workers the least authority they need, keep secrets and unrelated context out of their prompts, and place deterministic validation after generation wherever possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The coordinator should spend tokens on decisions
&lt;/h2&gt;

&lt;p&gt;Model routing is often described as infrastructure: gateways, provider tables, queues, and billing dashboards. Shunt demonstrates a smaller and more practical layer. A hook can intercept a costly action, a script can call a named worker, and a skill can teach the coordinator when that worker is appropriate.&lt;/p&gt;

&lt;p&gt;The important optimization is not simply replacing an expensive token with a cheaper token. It is changing what crosses the coordinator boundary. Whole files become findings. Repeated patterns become files on disk. The conversation retains intent, decisions, and the evidence necessary to verify them.&lt;/p&gt;

&lt;p&gt;That is a useful standard for any coding-agent workflow: pay the strongest model to resolve uncertainty. Do not pay it to be a pipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://engineering.atspotify.com/2026/9/portal-by-spotify-cut-my-claude-code-token-usage-by-90" rel="noopener noreferrer"&gt;Portal by Spotify cut my Claude Code token usage by 90%&lt;/a&gt;, the original experiment and its design constraints.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/spotify/portal-ai-plugins" rel="noopener noreferrer"&gt;Spotify Portal AI Plugins&lt;/a&gt;, including shunt’s hooks, scripts, skills, evaluations, and benchmark table.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://backstage.spotify.com/docs/portal/core-features-and-plugins/aika/modes" rel="noopener noreferrer"&gt;AiKA modes&lt;/a&gt;, the mode fields, MCP tool attachment, processors, visibility, and context controls.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Claude Code hooks&lt;/a&gt;, the lifecycle events and command-hook model used for enforcement.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/architecture" rel="noopener noreferrer"&gt;Model Context Protocol architecture&lt;/a&gt;, the host/client/server separation behind composable tools and context.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>GPT-6 Astra: OpenAI's New Model Is Built to Finish the Work</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/gpt-6-astra-openais-new-model-is-built-to-finish-the-work-4ma6</link>
      <guid>https://dev.to/0xgosu/gpt-6-astra-openais-new-model-is-built-to-finish-the-work-4ma6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu0qiq5u02722ao568o5l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu0qiq5u02722ao568o5l.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenAI released &lt;strong&gt;GPT-6 Astra&lt;/strong&gt; on September 3, 2026. It is the company’s new flagship model for complex reasoning, coding, computer use, scientific work, and the production of finished documents, spreadsheets, and presentations.&lt;/p&gt;

&lt;p&gt;The benchmark gains are large, but they are not the most important part of the release. Astra is designed around a broader unit of work. It can move through software, use tools, revise its approach, and keep track of a task across a long session. OpenAI is also making a stronger claim about judgment: the model should understand the boundary of a request, continue through routine uncertainty, and pause when a missing decision could materially change the result.&lt;/p&gt;

&lt;p&gt;That combination makes Astra feel less like a better answer engine and more like a computer operator that can be trusted with a larger assignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed with GPT-6 Astra
&lt;/h2&gt;

&lt;p&gt;GPT-6 Astra brings several improvements together in one model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State-of-the-art results reported by OpenAI across computer use, coding, science, cybersecurity, and professional work&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;1,050,000-token context window&lt;/strong&gt; and up to &lt;strong&gt;128,000 output tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Stronger visual judgment for websites, games, presentations, and other finished artifacts&lt;/li&gt;
&lt;li&gt;New API support for asynchronous tool calls and mid-turn steering&lt;/li&gt;
&lt;li&gt;More reliable behavior when instructions change during a long task&lt;/li&gt;
&lt;li&gt;New memory behavior in Codex for work that spans multiple context windows&lt;/li&gt;
&lt;li&gt;Stricter deployment safeguards after reaching OpenAI’s Critical cybersecurity capability threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model accepts text and image input and produces text output. It supports web search, file search, image generation, code execution, hosted shell, computer use, MCP, and other tools through the Responses API. Fine-tuning is not available at launch.&lt;/p&gt;

&lt;p&gt;For developers, the model ID is &lt;code&gt;gpt-6-astra&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Computer use is the center of the release
&lt;/h2&gt;

&lt;p&gt;OpenAI’s launch post leads with computer use because Astra is meant to complete work inside the same applications people use. The examples cover forms, CRM updates, calendar organization, research, frontend quality checks, spreadsheets, document formatting, CAD, game development, and scientific software.&lt;/p&gt;

&lt;p&gt;The reported results support that focus:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluation&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;th&gt;Claude Fable 5.1&lt;/th&gt;
&lt;th&gt;Claude Opus 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agents’ Last Exam&lt;/td&gt;
&lt;td&gt;59.3%&lt;/td&gt;
&lt;td&gt;53.6%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;55.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSWorld 2.0&lt;/td&gt;
&lt;td&gt;72.6%&lt;/td&gt;
&lt;td&gt;65.7%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;70.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScreenSpot-Pro&lt;/td&gt;
&lt;td&gt;92.7%&lt;/td&gt;
&lt;td&gt;76.9%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutomationBench&lt;/td&gt;
&lt;td&gt;41.4%&lt;/td&gt;
&lt;td&gt;18.1%&lt;/td&gt;
&lt;td&gt;31.4%&lt;/td&gt;
&lt;td&gt;26.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BenchCAD&lt;/td&gt;
&lt;td&gt;95.9%&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;td&gt;84.3%&lt;/td&gt;
&lt;td&gt;82.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2F0xgosu.dev%2Fblog%2Fopenai-gpt-6-astra-end-to-end-computer-work%2Fagentic-work-benchmarks.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2F0xgosu.dev%2Fblog%2Fopenai-gpt-6-astra-end-to-end-computer-work%2Fagentic-work-benchmarks.svg" alt="Grouped bar chart comparing GPT-6 Astra and GPT-5.6 Sol on computer-use and agentic-work benchmarks" width="1280" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OpenAI-reported results at the maximum evaluated effort. The chart is redrawn from the &lt;a href="https://openai.com/index/gpt-6-astra/" rel="noopener noreferrer"&gt;GPT-6 Astra launch article&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;OpenAI also reports that Astra completed its OSWorld tasks in about 47% less time than GPT-5.6 Sol in latency simulations. With an updated Codex harness, it completed tasks 1.9 times faster on Mind2Web.&lt;/p&gt;

&lt;p&gt;These are vendor-run evaluations, and production results will depend on the surrounding agent, tools, permissions, and safeguards. Still, the pattern matters. The largest gains appear in tasks that require the model to perceive an interface, decide what to do next, and carry out several actions correctly.&lt;/p&gt;

&lt;p&gt;This is where frontier models are heading. Generating the right instructions is useful. Opening the application, making the change, checking the result, and recovering from a mistake is a much larger product capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Professional output becomes part of the evaluation
&lt;/h2&gt;

&lt;p&gt;Astra is trained to produce artifacts that are closer to ready for use. OpenAI highlights its ability to follow an existing template, preserve a visual system, and create polished documents, spreadsheets, and presentations without filling them with unnecessary context.&lt;/p&gt;

&lt;p&gt;That sounds cosmetic until you consider how professional work is actually delivered. A financial model can contain correct formulas and still be difficult to review. A slide deck can contain accurate facts and still fail to communicate. A website can compile and still have broken spacing, weak hierarchy, or unusable controls.&lt;/p&gt;

&lt;p&gt;The last mile is part of the task. A model that can inspect its own rendered output and revise it can save more time than one that produces a strong first draft but leaves an hour of cleanup behind.&lt;/p&gt;

&lt;p&gt;OpenAI reports a smaller gain on its internal design benchmark, from 47.4% with GPT-5.6 Sol to 50.0% with Astra. That result is a useful reminder that visual quality remains difficult to measure and far from solved, even when individual demos look impressive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coding improves most where the environment matters
&lt;/h2&gt;

&lt;p&gt;OpenAI calls Astra its best software-engineering model so far. The detailed results show where the improvement is strongest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluation&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;th&gt;Claude Fable 5.1&lt;/th&gt;
&lt;th&gt;Claude Opus 5&lt;/th&gt;
&lt;th&gt;Gemini 3.8 Flash&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench 4.0&lt;/td&gt;
&lt;td&gt;57.9%&lt;/td&gt;
&lt;td&gt;37.3%&lt;/td&gt;
&lt;td&gt;55.8%&lt;/td&gt;
&lt;td&gt;52.3%&lt;/td&gt;
&lt;td&gt;19.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSWE 1.1&lt;/td&gt;
&lt;td&gt;74.1%&lt;/td&gt;
&lt;td&gt;72.7%&lt;/td&gt;
&lt;td&gt;67.4%&lt;/td&gt;
&lt;td&gt;73.7%&lt;/td&gt;
&lt;td&gt;73.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FrontierCode 1.1 Extended&lt;/td&gt;
&lt;td&gt;64.5%&lt;/td&gt;
&lt;td&gt;60.6%&lt;/td&gt;
&lt;td&gt;63.6%&lt;/td&gt;
&lt;td&gt;63.6%&lt;/td&gt;
&lt;td&gt;56.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal database migrations&lt;/td&gt;
&lt;td&gt;63.9%&lt;/td&gt;
&lt;td&gt;42.7%&lt;/td&gt;
&lt;td&gt;57.8%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The small DeepSWE increase suggests Astra is not equally better at every form of coding. The much larger Terminal-Bench and database-migration gains point to a different strength: operating in a real environment, coordinating several steps, and validating changes across a system.&lt;/p&gt;

&lt;p&gt;That distinction is important for engineering teams. The hardest part of a change is often not writing a function. It is finding the correct part of an unfamiliar repository, understanding its conventions, changing several connected pieces, running the right checks, and explaining the result clearly enough for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Long-running work gets a better memory system
&lt;/h2&gt;

&lt;p&gt;A million-token context window helps, but even a large context window eventually fills. Long coding and research sessions have traditionally used compaction: the system summarizes earlier work and continues from the summary. Each compaction risks losing a failed attempt, a subtle requirement, or the reason behind a decision.&lt;/p&gt;

&lt;p&gt;With Astra, Codex can keep notes across context windows and search earlier messages and tool output when it needs the original detail. The feature is experimental and can be enabled in Codex configuration before it becomes the default for Astra.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Long-context evaluation&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MRCR v2, 8-needle, 256K–512K&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;91.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRCR v2, 8-needle, 512K–1M&lt;/td&gt;
&lt;td&gt;96.3%&lt;/td&gt;
&lt;td&gt;73.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a meaningful architectural change. A compressed summary asks the model to predict everything it will need later. Searchable history lets it retrieve evidence after the need becomes clear.&lt;/p&gt;

&lt;p&gt;The result should be more useful on refactors, investigations, and other work where the path matters as much as the current state. It also makes auditing more important: persistent notes and searchable context need clear retention rules when the task contains sensitive code or business data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async tools and mid-turn steering make agents easier to direct
&lt;/h2&gt;

&lt;p&gt;GPT-6 Astra adds two API capabilities that change the interaction loop.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;asynchronous tool calling&lt;/strong&gt; , Astra can continue reasoning, call another tool, or answer an independent part of the request while an application runs a slow tool in the background. The application still executes the tool and returns its result using the original call ID.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;mid-turn steering&lt;/strong&gt; , a user can send a correction or new requirement while the model is working. Over a WebSocket connection, the Responses API preserves completed work and lets the model continue with the update instead of restarting the whole request.&lt;/p&gt;

&lt;p&gt;Developers can also change reasoning effort during a conversation with a &lt;code&gt;configuration_update&lt;/code&gt; item while preserving the prompt prefix for caching. Astra supports &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, and &lt;code&gt;max&lt;/code&gt; reasoning effort. It does not support &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These features make agents feel more like active collaborators. A long task does not have to be a sealed box that accepts one instruction and returns much later. The user can redirect it, answer a consequential question, or let independent work continue while a slow operation finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cybersecurity capability changes the deployment story
&lt;/h2&gt;

&lt;p&gt;Astra is the first OpenAI model to reach the &lt;strong&gt;Critical&lt;/strong&gt; level for cybersecurity capability under the company’s Preparedness Framework. OpenAI says an unsafeguarded version scored 100% on ExploitBench, discovered and used two previously unknown vulnerabilities during evaluation, and could develop exploits against hardened browsers and operating systems.&lt;/p&gt;

&lt;p&gt;That capability can help defenders review code and patch serious flaws. It also creates obvious misuse risks.&lt;/p&gt;

&lt;p&gt;The public version will refuse some advanced security requests, including creating proof-of-concept exploits for vulnerabilities. OpenAI plans to expand access for vetted defensive work through its Daybreak program. The deployment also includes monitoring that can pause or stop a task when the system detects potentially unauthorized behavior.&lt;/p&gt;

&lt;p&gt;OpenAI reports that Astra is better at staying inside the user’s authorized scope. In one internal evaluation, an unsafeguarded GPT-5.6 Sol went beyond the intended target 48% of the time, while Astra did so in 0% of cases. The launch post also says Astra never tried to bypass a Codex Auto-Review denial in another internal test.&lt;/p&gt;

&lt;p&gt;Those are encouraging results, but one caveat deserves equal attention. OpenAI found Astra’s written reasoning harder to monitor than GPT-5.6 Sol’s when researchers explicitly instructed the models to evade monitoring. The company says the model still struggles to hide the reasoning required for complex tasks and is developing auditing methods that do not depend only on written reasoning.&lt;/p&gt;

&lt;p&gt;The release therefore contains two safety stories at once: the model is better at following boundaries, and its growing capability makes failures more consequential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Science moves from answers to active research workflows
&lt;/h2&gt;

&lt;p&gt;Astra posts strong results in mathematics and scientific work, including 97.6% on FrontierMath Tier 4, 96.0% on GPQA Diamond, and 64.6% on Terminal-Bench Science 0.1. OpenAI also says the model helped improve published bounds for both short and large gaps between prime numbers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluation&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;th&gt;Claude Fable 5.1&lt;/th&gt;
&lt;th&gt;Claude Opus 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench Science 0.1&lt;/td&gt;
&lt;td&gt;64.6%&lt;/td&gt;
&lt;td&gt;22.4%&lt;/td&gt;
&lt;td&gt;52.6%&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FrontierMath Tier 4&lt;/td&gt;
&lt;td&gt;97.6%&lt;/td&gt;
&lt;td&gt;83.0%&lt;/td&gt;
&lt;td&gt;87.8%&lt;/td&gt;
&lt;td&gt;73.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPQA Diamond&lt;/td&gt;
&lt;td&gt;96.0%&lt;/td&gt;
&lt;td&gt;94.6%&lt;/td&gt;
&lt;td&gt;93.7%&lt;/td&gt;
&lt;td&gt;93.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HealthBench Professional&lt;/td&gt;
&lt;td&gt;63.4%&lt;/td&gt;
&lt;td&gt;60.5%&lt;/td&gt;
&lt;td&gt;58.1%&lt;/td&gt;
&lt;td&gt;56.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2F0xgosu.dev%2Fblog%2Fopenai-gpt-6-astra-end-to-end-computer-work%2Fscience-security-benchmarks.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2F0xgosu.dev%2Fblog%2Fopenai-gpt-6-astra-end-to-end-computer-work%2Fscience-security-benchmarks.svg" alt="Grouped bar chart comparing GPT-6 Astra and GPT-5.6 Sol on scientific and cybersecurity benchmarks" width="1280" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OpenAI-reported results. Cybersecurity evaluations shown here used models without production safeguards. The chart is redrawn from the &lt;a href="https://openai.com/index/gpt-6-astra/" rel="noopener noreferrer"&gt;GPT-6 Astra launch article&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The practical shift is the combination of reasoning and software use. A research model can inspect data in specialized tools, write and run code, generate plots, test an idea, and help a researcher decide what to investigate next.&lt;/p&gt;

&lt;p&gt;That does not make the model an independent scientist. Proofs still need verification, experiments need reproducibility, and conclusions need domain experts. It does make the useful unit of assistance much larger than answering a technical question from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context, price, and availability
&lt;/h2&gt;

&lt;p&gt;GPT-6 Astra is initially rolling out to a limited set of organizations. OpenAI says access will expand over the following days to ChatGPT Plus, Pro, Business, and Enterprise plans, the OpenAI API, and AWS. Enterprise administrators must enable it for their workspaces at launch. Pro, Business, and Enterprise users will also receive access to GPT-6 Astra Pro.&lt;/p&gt;

&lt;p&gt;Standard API pricing per one million tokens is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached input&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache writes&lt;/td&gt;
&lt;td&gt;$12.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$50.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prompts above 272,000 input tokens cost twice the input and cache rates and 1.5 times the output rate for the entire request. Batch and Flex processing are priced at half the Standard rates. Fast mode offers up to twice the speed at twice the Standard price, though OpenAI documents restrictions for EU data residency.&lt;/p&gt;

&lt;p&gt;The 1,050,000-token window is valuable, but it should not be treated as free storage. Long prompts can become expensive quickly, especially after the higher long-context rate begins. Retrieval, caching, and deliberate context selection still matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would evaluate Astra
&lt;/h2&gt;

&lt;p&gt;The best test is a complete unit of work that your current model struggles to finish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give it a bug that crosses several services and requires browser or terminal verification.&lt;/li&gt;
&lt;li&gt;Ask it to update a polished document or spreadsheet while preserving an existing template.&lt;/li&gt;
&lt;li&gt;Interrupt a long run with a changed requirement and check whether it preserves the original constraints.&lt;/li&gt;
&lt;li&gt;Compare reasoning efforts on quality, elapsed time, token use, retries, and human corrections.&lt;/li&gt;
&lt;li&gt;Test legitimate security tasks to learn where safeguards pause or block the workflow.&lt;/li&gt;
&lt;li&gt;Review whether its notes and final report make the full run easy to audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Track &lt;strong&gt;cost per accepted result&lt;/strong&gt; , not only cost per token. Astra is expensive, but a higher token price can still be economical if the model finishes more work, uses fewer output tokens, and needs fewer restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical takeaway
&lt;/h2&gt;

&lt;p&gt;GPT-6 Astra is OpenAI’s clearest attempt yet to turn a frontier model into a reliable operator for end-to-end work. Its strongest improvements appear where reasoning meets an environment: navigating software, coordinating tools, preserving context, producing finished artifacts, and adjusting to new instructions without losing the task.&lt;/p&gt;

&lt;p&gt;The release also shows the cost of that progress. Cybersecurity capability has reached a threshold that requires stricter access and live monitoring, while written reasoning has become harder to inspect in some adversarial tests.&lt;/p&gt;

&lt;p&gt;Astra should be evaluated as a system, not a chatbot. The model, harness, tools, permissions, memory, and safeguards all shape the result. For teams already building long-running agents, that system-level improvement is the reason to test it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openai.com/index/gpt-6-astra/" rel="noopener noreferrer"&gt;OpenAI: GPT-6 Astra — A new generation of intelligence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/models/gpt-6-astra" rel="noopener noreferrer"&gt;OpenAI API: GPT-6 Astra model details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/latest-model?model=gpt-6-astra" rel="noopener noreferrer"&gt;OpenAI API: Using GPT-6 Astra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/index/safety-overview-gpt-6-astra/" rel="noopener noreferrer"&gt;OpenAI: Safety overview for GPT-6 Astra&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Audacity 4: Rebuilding an Audio Editor Without Losing Its Muscle Memory</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/audacity-4-rebuilding-an-audio-editor-without-losing-its-muscle-memory-425l</link>
      <guid>https://dev.to/0xgosu/audacity-4-rebuilding-an-audio-editor-without-losing-its-muscle-memory-425l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz8fqhzz6qndq0q71zft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz8fqhzz6qndq0q71zft.png" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Audacity 4 is not a coat of paint on a familiar waveform editor. The application interface has been rebuilt on Qt, clips have become objects that can be selected and manipulated directly, project files now use the &lt;code&gt;.aup4&lt;/code&gt; format, and several long-standing tools have been replaced by context-sensitive behavior.&lt;/p&gt;

&lt;p&gt;That is an unusually large change for software whose greatest feature may be familiarity. Audacity succeeds because people can install it, record or open a file, make a precise change, and export the result without first adopting a production system. Its interface grew over decades around that directness. Replacing the foundation risks disturbing thousands of small habits even when every individual redesign looks sensible.&lt;/p&gt;

&lt;p&gt;The important question is therefore not whether version 4 looks modern. It is whether a mature desktop application can change its internal and interaction architecture while keeping the short path from sound to finished file intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interface migration is an architectural change
&lt;/h2&gt;

&lt;p&gt;Audacity 3 used wxWidgets for its cross-platform user interface. Audacity 4 moves the interface to Qt and QML, building substantial parts on the same Muse Framework used by MuseScore Studio. The project’s &lt;a href="https://github.com/audacity/audacity/blob/master/BUILDING.md" rel="noopener noreferrer"&gt;build instructions&lt;/a&gt; now call for Qt 6, recommend Qt Creator for QML work, and explicitly note the MuseScore foundation.&lt;/p&gt;

&lt;p&gt;This matters beyond rounded controls and dark mode. A UI toolkit determines how an application lays out windows, renders at different display densities, routes keyboard focus, exposes controls to accessibility tools, and separates visual state from domain logic. Moving toolkits changes the seams at which all of those behaviors meet.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Faudacity-4-editor-rebuild%2Fapplication-layers.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Faudacity-4-editor-rebuild%2Fapplication-layers.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The visible Qt/QML interface sits on a broader migration of application services and editing boundaries; the audio engine remains the workhorse below it.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Qt gives Audacity native high-DPI rendering, dockable and floating panels, saveable workspaces, and a consistent theming surface. The shipped Modern, Classic, and Music workspaces are more than presets. They acknowledge that one permanent layout cannot serve a quick voice edit, a multitrack music session, and a long-time user’s muscle memory equally well.&lt;/p&gt;

&lt;p&gt;The Classic option is especially important. Compatibility in mature tools is not limited to opening old files. Users also carry a spatial map: transport controls live here, meters sit there, and a repeated task follows a known sequence. A workspace that restores familiar positions lowers migration cost without forcing the new interface to remain frozen in the old architecture.&lt;/p&gt;

&lt;p&gt;There is a trade-off for contributors and downstream packagers. The new build has a broad Qt and Muse Framework dependency graph, and the official instructions say some inherited MuseScore dependencies have not yet been cleaned up. A richer application framework makes features such as docking, command routing, and reusable controls easier to build, but it also expands the surface that must compile and behave correctly across Windows, macOS, and Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clips are finally the unit of editing
&lt;/h2&gt;

&lt;p&gt;The more consequential user-facing change is the clip model. In Audacity 4, a clip can be clicked directly, Shift-clicked into a multi-selection, grouped with other clips, moved between compatible mono and stereo tracks, trimmed, or time-stretched together with the rest of a selection.&lt;/p&gt;

&lt;p&gt;This sounds ordinary because most timeline editors train users to see a clip as a thing. Earlier Audacity workflows often made the time selection primary: first mark a range on the waveform, then choose what operation applies to the audio under that range. Version 4 makes both concepts explicit. A time selection describes an interval; a clip selection identifies the objects an edit should affect.&lt;/p&gt;

&lt;p&gt;That distinction resolves several awkward operations. Moving three non-adjacent clips no longer requires arranging them into a special contiguous state. Grouping gives a persistent relationship instead of asking the user to reconstruct the selection. Trim and stretch handles reveal which transformations belong to the selected object. Alignment guides and sample-boundary snapping make placement rules visible at the moment they matter.&lt;/p&gt;

&lt;p&gt;The new freedom also needs a clear collision rule. Audacity now allows one clip to be moved over another; the moved clip replaces the overlapped part instead of refusing the operation. This is efficient once learned, but destructive-looking behavior deserves care. Users should know whether hidden material remains recoverable, which clip has priority, and how Undo restores the previous state.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Faudacity-4-editor-rebuild%2Fclip-editing-flow.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Faudacity-4-editor-rebuild%2Fclip-editing-flow.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;Direct selection makes the clip the object of the command; context and collision rules decide how that command changes the timeline.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The release also turns splitting into an explicit gesture: hold or press &lt;code&gt;S&lt;/code&gt;, then click the waveform or clip header. Related commands cover split-cut, split-delete, splitting at silences, and splitting to a new track. Paste can create a track when needed and adapt compatible channel layouts. These are small reductions in ceremony, but quick editors are built from exactly this kind of accumulated friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context replaces the old tool switch
&lt;/h2&gt;

&lt;p&gt;Audacity’s separate Select, Envelope, Draw, and Multi-tool modes are gone. Their functions now emerge from context.&lt;/p&gt;

&lt;p&gt;Zoom to individual samples and drawing becomes available. Show clip gain and the envelope appears as a visual layer. Hold &lt;code&gt;S&lt;/code&gt; and a click becomes a split. Click a clip header and the clip becomes the target. The interface asks the pointer position, zoom level, visible layer, and modifier keys to determine what the user intends.&lt;/p&gt;

&lt;p&gt;This reduces mode errors—the familiar experience of dragging while the wrong tool is active—but it can introduce discoverability errors. A toolbar announces the tools it contains. Context-sensitive behavior is quieter. It works beautifully after it becomes habit and can be invisible before that point.&lt;/p&gt;

&lt;p&gt;Good contextual interfaces therefore need strong feedback. Cursors must change before the click. Handles must appear on selection. Sample points must become visibly editable at the required zoom. Shortcut hints and transition documentation must make hidden capabilities searchable. Audacity’s &lt;a href="https://www.audacityteam.org/manual/new-in-audacity-4/audacity-3-to-4-transition-guide/" rel="noopener noreferrer"&gt;3-to-4 transition guide&lt;/a&gt; performs part of that job by mapping old modes to new gestures rather than simply listing features.&lt;/p&gt;

&lt;p&gt;The removal of Sync-Lock follows the same philosophy. Instead of keeping a persistent special mode, delete, cut, and paste have explicit variants for either leaving a gap or moving later material to preserve timing. The decision moves closer to the operation that needs it. That is easier to reason about locally, although users who relied on a project-wide Sync-Lock state must relearn the commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realtime effects change the cost of experimentation
&lt;/h2&gt;

&lt;p&gt;Audacity still supports destructive effects: select audio, apply the effect, and change the waveform. Version 4 also emphasizes realtime effect stacks on tracks and the master output. These effects are calculated during playback, remain adjustable, can be reordered, and are not written into the audio until export.&lt;/p&gt;

&lt;p&gt;That changes editing from a sequence of commitments into a set of parameters. A noise treatment can be dialed back later. Compression can move before or after equalization. A master effect can shape the complete mix without rendering every source again.&lt;/p&gt;

&lt;p&gt;The benefit is not merely Undo. Undo walks backward through history, potentially discarding later work. A realtime stack keeps the decision live in the current project state. The editor can revisit one parameter without rewinding unrelated edits.&lt;/p&gt;

&lt;p&gt;The cost moves to playback. Realtime processing must finish each audio buffer before the device needs it. A large stack, expensive plugin, or small buffer can exhaust the timing budget and cause dropouts. Audacity must coordinate UI changes, project state, plugin hosting, meters, and the audio thread without allowing interface work to interrupt sound. The new track-header meters and consistent preset handling make this system easier to inspect, but the hard constraint remains: audio deadlines do not wait for the UI.&lt;/p&gt;

&lt;p&gt;Supported plugin boundaries also changed. The 4.0 release notes list VST3, Nyquist, LV2 on Linux, and Audio Units on macOS, while the transition guide describes the formats currently available to users. The release explicitly says VAMP and LADSPA hosting, along with some analyzers and effects, are not yet present. Anyone whose workflow depends on a particular plugin should verify it before converting an active project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playback and recording become timeline-native
&lt;/h2&gt;

&lt;p&gt;The playhead now remains visible during navigation and can be dragged. Playback can seek without stopping. Recording can begin at any point on the timeline and creates a clip there. Loop boundaries and selections have been revised so these different ranges are easier to control independently.&lt;/p&gt;

&lt;p&gt;These changes make transport behave more like editing. The timeline is not just a picture underneath play and record buttons; it is the shared coordinate system for navigation, loops, selections, clips, and new recordings.&lt;/p&gt;

&lt;p&gt;Audio Setup has also been rebuilt around real device behavior. It can show system-default devices, refresh lists, map channels, and follow operating-system device changes. Official Windows builds now include ASIO playback and recording. For users with compatible interfaces, that can mean lower-latency and multichannel access without compiling a custom build.&lt;/p&gt;

&lt;p&gt;None of this removes the need to test a real setup. Audio applications sit at the meeting point of drivers, permissions, sample rates, channel layouts, plugins, and hardware clocks. A release note can promise that the path exists; only a rehearsal proves that a particular microphone, interface, and monitoring arrangement works together.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;.aup4&lt;/code&gt; draws a one-way project boundary
&lt;/h2&gt;

&lt;p&gt;Audacity 4 saves projects as &lt;code&gt;.aup4&lt;/code&gt;. It opens &lt;code&gt;.aup3&lt;/code&gt; projects and converts them without changing the original, and it can import still older &lt;code&gt;.aup&lt;/code&gt; projects. The converted work cannot be saved back to &lt;code&gt;.aup3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That one-way boundary is the correct place to be conservative. A project file is not an exported WAV or MP3. It contains editable structure: tracks, clips, hidden trimmed audio, effect configuration, appearance data, preview thumbnails, and undo-related state. Saving a new model into an old schema would either lose information silently or require the old application to understand concepts it never had.&lt;/p&gt;

&lt;p&gt;The safe migration workflow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the original &lt;code&gt;.aup3&lt;/code&gt; file and its backups.&lt;/li&gt;
&lt;li&gt;Open a copy in Audacity 4 and save the converted &lt;code&gt;.aup4&lt;/code&gt; project under a new name.&lt;/li&gt;
&lt;li&gt;Check clip positions, envelopes, effects, labels, sample rate, and project duration.&lt;/li&gt;
&lt;li&gt;Export a reference mix and compare it with the version produced by Audacity 3.&lt;/li&gt;
&lt;li&gt;Convert important projects before uninstalling the older application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The official &lt;a href="https://www.audacityteam.org/manual/getting-started/save-your-project/" rel="noopener noreferrer"&gt;saving guide&lt;/a&gt; says an &lt;code&gt;.aup4&lt;/code&gt; file holds the editable project and distinguishes that from exported audio. That distinction is easy to overlook. An export is the deliverable; the project is the machinery required to change it. Migration testing must cover both.&lt;/p&gt;

&lt;h2&gt;
  
  
  A major release includes deliberate gaps
&lt;/h2&gt;

&lt;p&gt;Audacity 4.0 does not pretend to be a strict superset of Audacity 3. The release notes identify Time Tracks, note/MIDI tracks, the Mixer, Macro Manager and scripting pipe, VAMP and LADSPA hosting, and play-at-speed as unavailable at launch. Sync-Lock and the old tool modes are intentionally replaced rather than scheduled for restoration. Some export, rendering, analysis, and effect features are also missing.&lt;/p&gt;

&lt;p&gt;This is the uncomfortable arithmetic of a foundation rewrite. Waiting for perfect parity keeps the new architecture away from real workloads; releasing earlier gives users the architecture while asking some of them to remain on version 3. The honest approach is a visible compatibility list and a reversible trial.&lt;/p&gt;

&lt;p&gt;For a quick two-track edit, version 4’s direct clips and cleaner interface may make the decision easy. For a scripted production pipeline that drives the old command pipe, the missing Macro Manager can be a blocker. For an accessibility-dependent workflow, the relevant question is not visual polish but whether keyboard navigation, focus order, screen-reader labels, and high-contrast behavior work for the exact task.&lt;/p&gt;

&lt;p&gt;Version numbers do not decide those cases. Workflows do.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate the upgrade
&lt;/h2&gt;

&lt;p&gt;Treat Audacity 4 as a new editing environment that can import old work, not as an invisible point update.&lt;/p&gt;

&lt;p&gt;Start with a disposable project representative of what you actually do. Record through the real device. Move and overlap clips. Exercise ripple-style deletion. Load the effects and plugins you depend on. Save, close, recover, reopen, and export. Test keyboard-only operation if shortcuts matter. If collaboration depends on project exchange, make sure every participant understands that &lt;code&gt;.aup4&lt;/code&gt; cannot return to version 3.&lt;/p&gt;

&lt;p&gt;Then separate three kinds of compatibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data compatibility:&lt;/strong&gt; Does the old project convert with the expected structure and sound?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow compatibility:&lt;/strong&gt; Can you complete the job without a missing tool or automation path?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational compatibility:&lt;/strong&gt; Do devices, plugins, latency, stability, packaging, and accessibility meet production needs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Audacity 4’s achievement is not that all three answers are automatically yes. It is that the project has established a new platform on which those answers can improve: a modern UI stack, clearer editing objects, explicit command routing, flexible workspaces, realtime processing, and a project format able to carry the new state.&lt;/p&gt;

&lt;p&gt;The release preserves Audacity’s essential proposition. Audio stays editable on the user’s machine, the waveform remains the center of the work, and the shortest useful path is still open, edit, export. Almost everything around that path has been rebuilt. The next test is whether the new foundation can remain as approachable as the old one while becoming easier to extend for the decade ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/audacity/audacity/releases/tag/Audacity-4.0.0" rel="noopener noreferrer"&gt;Audacity 4.0 release notes&lt;/a&gt;, the complete feature and compatibility list.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.audacityteam.org/manual/new-in-audacity-4/audacity-3-to-4-transition-guide/" rel="noopener noreferrer"&gt;Audacity 3 to 4 transition guide&lt;/a&gt;, a workflow map for existing users.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/audacity/audacity/blob/master/BUILDING.md" rel="noopener noreferrer"&gt;Building Audacity&lt;/a&gt;, the Qt, QML, CMake, and Muse Framework development setup.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.audacityteam.org/manual/getting-started/save-your-project/" rel="noopener noreferrer"&gt;Save your project&lt;/a&gt;, the distinction between editable &lt;code&gt;.aup4&lt;/code&gt; projects and exported audio.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/audacity/audacity" rel="noopener noreferrer"&gt;Audacity source repository&lt;/a&gt;, for code, issues, and contribution guidance.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Claude Fable 5.1: Frontier Intelligence Gets Cheaper to Keep Running</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Wed, 02 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/claude-fable-51-frontier-intelligence-gets-cheaper-to-keep-running-m8p</link>
      <guid>https://dev.to/0xgosu/claude-fable-51-frontier-intelligence-gets-cheaper-to-keep-running-m8p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylzsvcvi5egogoeqlr2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylzsvcvi5egogoeqlr2l.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anthropic released &lt;strong&gt;Claude Fable 5.1&lt;/strong&gt; on September 1, 2026. It is the company’s most capable generally available model, built for coding, research, and long-running work that can stretch across hours, applications, and large amounts of context.&lt;/p&gt;

&lt;p&gt;The obvious story is that Fable got smarter. The more useful story is that it became cheaper to keep working.&lt;/p&gt;

&lt;p&gt;Fable 5.1 keeps the same base API price as Fable 5: &lt;strong&gt;$10 per million input tokens&lt;/strong&gt; and &lt;strong&gt;$50 per million output tokens&lt;/strong&gt;. But cache reads now cost &lt;strong&gt;$0.25 per million tokens&lt;/strong&gt; , down 75%. Anthropic estimates that this reduces the total cost of typical workloads by about 25%, and highly agentic workloads by as much as 45%.&lt;/p&gt;

&lt;p&gt;That matters because long-running agents repeatedly revisit the same context. Their economics are shaped less by a single prompt than by how cheaply they can remember, inspect, verify, and continue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Fable 5.1
&lt;/h2&gt;

&lt;p&gt;Claude Fable 5.1 is aimed at work where mistakes compound over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Features that span an entire codebase&lt;/li&gt;
&lt;li&gt;Multi-day coding and research sessions&lt;/li&gt;
&lt;li&gt;Browser and computer-use workflows&lt;/li&gt;
&lt;li&gt;Complex analysis across documents, charts, and tables&lt;/li&gt;
&lt;li&gt;Unattended agents that need to recover from failures&lt;/li&gt;
&lt;li&gt;Scientific problems that require tools, experiments, and iteration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic says the model is less likely to take shortcuts that produce superficially correct work. It is designed to find root causes, verify its own output, and keep its progress understandable during long tasks.&lt;/p&gt;

&lt;p&gt;The launch also makes effort settings more important. Fable 5.1 defaults to &lt;strong&gt;High effort in Claude Code&lt;/strong&gt; and &lt;strong&gt;Medium effort in &lt;a href="http://claude.ai/" rel="noopener noreferrer"&gt;Claude.ai&lt;/a&gt; and Claude Cowork&lt;/strong&gt;. Anthropic reports that Low and Medium effort can match or exceed Fable 5 on several evaluations at a lower cost.&lt;/p&gt;

&lt;p&gt;This gives teams a more useful control than simply choosing between a weak model and a strong one. The same model can handle routine steps economically, then spend more effort when a task reaches a genuinely difficult decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark gains are broad
&lt;/h2&gt;

&lt;p&gt;Anthropic’s published results show improvements across coding, computer use, knowledge work, and scientific research:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluation&lt;/th&gt;
&lt;th&gt;Fable 5.1&lt;/th&gt;
&lt;th&gt;Fable 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench-Science 0.1&lt;/td&gt;
&lt;td&gt;52.6%&lt;/td&gt;
&lt;td&gt;24.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench 4.0&lt;/td&gt;
&lt;td&gt;55.8%&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutomationBench&lt;/td&gt;
&lt;td&gt;31.4%&lt;/td&gt;
&lt;td&gt;17.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CursorBench 3.2.0&lt;/td&gt;
&lt;td&gt;73.4%&lt;/td&gt;
&lt;td&gt;70.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humanity’s Last Exam, with tools&lt;/td&gt;
&lt;td&gt;65.0%&lt;/td&gt;
&lt;td&gt;63.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Terminal-Bench-Science result stands out: Fable 5.1 more than doubles Anthropic’s reproduced Fable 5 score. AutomationBench also shows a large jump, which supports the model’s positioning around multi-step business workflows rather than one-shot answers.&lt;/p&gt;

&lt;p&gt;These are vendor-reported results, not a substitute for testing the model on your own work. Anthropic also notes that safety interventions affected some benchmark runs. On certain flagged tasks, the model received a zero or the request was completed by an Opus fallback. The exact harness and safeguard behavior are part of the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache pricing is the real agent upgrade
&lt;/h2&gt;

&lt;p&gt;The 75% cache-read price cut is easy to overlook next to the benchmark table, but it may have the largest production impact.&lt;/p&gt;

&lt;p&gt;An agent working on a large repository may repeatedly reuse system instructions, tool definitions, source files, earlier decisions, and test output. Without caching, every turn pays to process that context again. With cheap cache reads, the cost shifts toward the new work being done rather than the history required to do it coherently.&lt;/p&gt;

&lt;p&gt;That changes which tasks are practical to delegate. A two-minute code edit will not benefit much. A six-hour investigation that repeatedly checks the same codebase might.&lt;/p&gt;

&lt;p&gt;The right metric is therefore not price per token. It is &lt;strong&gt;cost per completed task&lt;/strong&gt;. Fable 5.1 is still expensive at the token level, but it may be cheaper when a stronger model uses fewer tokens, needs fewer retries, and reads most of its context from cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fable and Mythos are still the same model underneath
&lt;/h2&gt;

&lt;p&gt;Anthropic launched &lt;strong&gt;Claude Mythos 5.1&lt;/strong&gt; alongside Fable 5.1. The two use the same underlying model but have different safeguards and access rules.&lt;/p&gt;

&lt;p&gt;Fable 5.1 is generally available. Mythos 5.1 is limited to vetted cybersecurity and life sciences users through trusted-access programs. Its more permissive safeguards are designed for work that the public Fable model may block or route elsewhere.&lt;/p&gt;

&lt;p&gt;For most developers, Fable 5.1 is the model that matters. But the split explains some behavior that would otherwise look inconsistent: a model may be capable of a task while the product deliberately restricts how that capability can be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cybersecurity safeguards should interrupt less often
&lt;/h2&gt;

&lt;p&gt;The original Fable 5 launch made refusals and fallbacks a major integration concern. Fable 5.1 tries to make those controls more precise.&lt;/p&gt;

&lt;p&gt;Anthropic says its updated cybersecurity safeguards produce about &lt;strong&gt;60% fewer interventions per Claude Code session&lt;/strong&gt; than the safeguards used with Fable 5. Fable 5.1 can now identify software vulnerabilities for defensive work. Tasks such as exploit generation, penetration testing, and binary-based vulnerability scanning may still be routed to an Opus model.&lt;/p&gt;

&lt;p&gt;Biology safeguards have also been tuned. Anthropic says they fire 85% less often on benign elementary biology and medical questions than the original Fable 5 safeguards, while research and development work in the life sciences can still fall back to Opus.&lt;/p&gt;

&lt;p&gt;In Claude’s consumer and business apps, those fallbacks happen automatically. API users need to configure the Fallback API. Production integrations should test that path explicitly instead of assuming every request will be handled by Fable 5.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scientific work is a preview of a different kind of model
&lt;/h2&gt;

&lt;p&gt;Anthropic devoted a large part of the release to scientific research. One example used Fable 5.1 to train a neural network that produced a new elevation map covering a third of Venus. According to Anthropic, the map reveals details at a scale of two to three kilometers instead of 10 to 20 kilometers and improves height accuracy by up to 25%.&lt;/p&gt;

&lt;p&gt;Mythos 5.1 was tested on protein-binder design and GPU optimization for biology models. Anthropic reports that it produced high-affinity binders and accelerated seven open-source deep-learning models by up to 2.5 times with identical outputs.&lt;/p&gt;

&lt;p&gt;These examples are not proof that an LLM can independently do science. They are evidence that the useful unit is becoming larger. The model is not only answering a scientific question; it is writing code, operating tools, running experiments, checking results, and producing an artifact that researchers can validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data retention still needs a policy review
&lt;/h2&gt;

&lt;p&gt;Fable 5.1 requires &lt;strong&gt;30-day data retention by default&lt;/strong&gt; for safety monitoring. That is not a minor implementation detail for teams working with source code, financial data, legal documents, or health information.&lt;/p&gt;

&lt;p&gt;Anthropic is introducing &lt;strong&gt;Enterprise Frontier Safeguards&lt;/strong&gt; , which keep customer data in infrastructure controlled by the customer while still applying automated misuse detection. The system will roll out in phases beginning later in 2026. Eligible enterprise customers can use Fable 5.1 with zero data retention until it becomes available.&lt;/p&gt;

&lt;p&gt;Before upgrading, confirm the retention policy that applies to your account and deployment surface. A better model is not a drop-in replacement if it changes where sensitive context is stored or reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would evaluate the upgrade
&lt;/h2&gt;

&lt;p&gt;I would not start with a generic chatbot comparison. Fable 5.1 is designed to separate itself on long, difficult work, so test it there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give it a bug that crosses several services and requires root-cause analysis.&lt;/li&gt;
&lt;li&gt;Run a code review that includes architecture, tests, and performance concerns.&lt;/li&gt;
&lt;li&gt;Compare Medium and High effort on the same task.&lt;/li&gt;
&lt;li&gt;Measure total tokens, cache reads, retries, elapsed time, and human corrections.&lt;/li&gt;
&lt;li&gt;Test safety fallback behavior with legitimate security-related prompts.&lt;/li&gt;
&lt;li&gt;Check whether the final report makes a long run easier to audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important question is not whether Fable 5.1 writes a better first answer. It is whether it can finish a larger unit of work with fewer interventions and enough evidence that a human can trust the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical takeaway
&lt;/h2&gt;

&lt;p&gt;Claude Fable 5.1 looks like a refinement of Anthropic’s frontier-model strategy rather than a new tier. The public model gets stronger, its safeguards become less disruptive, and the cost structure improves specifically for context-heavy agents.&lt;/p&gt;

&lt;p&gt;That combination is more meaningful than a benchmark win by itself. Long-running AI systems do not fail only because the model is not intelligent enough. They fail because context becomes expensive, tools go wrong, safeguards trigger unpredictably, and humans cannot tell what happened during the run.&lt;/p&gt;

&lt;p&gt;Fable 5.1 addresses each of those pressures. Whether it does so well enough for your workload is something only a real evaluation can answer. But if Fable 5 was already close to useful, the 5.1 release is worth testing now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/claude-fable-and-mythos-5-1" rel="noopener noreferrer"&gt;Anthropic: Claude Fable 5.1 and Claude Mythos 5.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/claude/fable" rel="noopener noreferrer"&gt;Claude Fable model overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/" rel="noopener noreferrer"&gt;Claude Platform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>How RuneScape Turned 56k Limits Into a Multiplayer Protocol</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Wed, 02 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/how-runescape-turned-56k-limits-into-a-multiplayer-protocol-5dic</link>
      <guid>https://dev.to/0xgosu/how-runescape-turned-56k-limits-into-a-multiplayer-protocol-5dic</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ty34psu0tqzxjp5njmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ty34psu0tqzxjp5njmj.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 2004, a browser could hold an inhabited 3D world on the far side of a telephone line. RuneScape did not make that possible by finding one miraculous compression algorithm. It made thousands of small agreements between the client and server, then stopped sending facts that both sides already knew.&lt;/p&gt;

&lt;p&gt;The result is easiest to understand through one ordinary action: click the tile immediately north of your character. In the protocol examined by James Monger from a decompiled 2004 client, that request costs seven bytes. After the server accepts the movement, a nearby player can learn about it through a scene update of roughly nine bytes. Sixteen application bytes complete the useful round trip.&lt;/p&gt;

&lt;p&gt;Those figures are not the whole cost of TCP, nor are they a universal measurement for every RuneScape revision. They are a clean window into the design. The client finds paths locally. The server validates them against its own collision map. Both sides retain a synchronized model of the visible world. High-frequency state is encoded in individual bits, while larger, rarer detail records stay byte-aligned so the server can cache and copy them.&lt;/p&gt;

&lt;p&gt;This is more than nostalgia for heroic byte shaving. It is a practical example of choosing a protocol around its actual value ranges, repetition patterns, latency model, and compute budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the real constraints
&lt;/h2&gt;

&lt;p&gt;A 56k modem’s advertised rate was measured in kilobits, not kilobytes. After line conditions and protocol overhead, treating the downstream budget as roughly 5 KB/s is a useful planning number. Upstream was tighter. A crowded scene, chat, combat, inventory changes, map loading, and control traffic all had to share it.&lt;/p&gt;

&lt;p&gt;RuneScape 2 also ran as a Java applet. The sandbox was valuable to users because downloaded code could not freely inspect the machine. It constrained the network design at the same time. Oracle’s archived &lt;a href="https://docs.oracle.com/javase/tutorial/deployment/applet/security.html" rel="noopener noreferrer"&gt;applet security documentation&lt;/a&gt; describes sandboxed applets as able to connect back to their origin while being blocked from arbitrary third-party hosts and native libraries. RuneScape’s game traffic therefore used a regular TCP connection rather than a custom native UDP stack.&lt;/p&gt;

&lt;p&gt;The third constraint was deliberate: the game world advanced in cycles of roughly 600 milliseconds. A command that arrived just before the next cycle might run almost immediately. One arriving just after could wait nearly a full tick. Once that rhythm was part of the game, reducing a client flush from tens of milliseconds to a handful did not transform the response time. The worthwhile optimization target was bandwidth.&lt;/p&gt;

&lt;p&gt;That distinction matters. A team can spend months optimizing a layer that is hidden under a larger scheduling quantum. RuneScape’s protocol was shaped around the bottleneck that remained visible: the number of bytes multiplied across every player and every tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  A click begins as local computation
&lt;/h2&gt;

&lt;p&gt;The browser already had a collision map for the loaded area. When the player clicked a destination, the client ran a breadth-first search locally and constructed a route. It did not upload a continuous stream of joystick positions, and it did not send every traversed tile.&lt;/p&gt;

&lt;p&gt;The walk request carried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one opcode byte identifying the operation;&lt;/li&gt;
&lt;li&gt;one byte giving the variable body length;&lt;/li&gt;
&lt;li&gt;two 16-bit values for the first waypoint’s absolute coordinates;&lt;/li&gt;
&lt;li&gt;two signed bytes for each later waypoint, expressed as an offset from the first; and&lt;/li&gt;
&lt;li&gt;one byte for the Ctrl-key movement modifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a single step, there are no additional waypoint deltas. The body is five bytes, and the opcode plus length marker bring the packet to seven.&lt;/p&gt;

&lt;p&gt;Longer paths stay compact for two reasons. First, later waypoints are deltas, so each axis needs one byte instead of two. Second, the path contains corners rather than every tile. Ten unobstructed tiles in a straight line still need only the endpoint. The server knows the starting position and owns an equivalent collision map, so it can reconstruct and validate the intervening movement.&lt;/p&gt;

&lt;p&gt;This is semantic compression. The protocol is not trying to squeeze an arbitrary coordinate stream after it has been produced. It changes what is produced. A path is represented by the smallest set of decisions needed to recreate it.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Frunescape-56k-protocol%2Fwalk-tick-round-trip.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Frunescape-56k-protocol%2Fwalk-tick-round-trip.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The browser computes a proposal, the server validates authoritative state, and observers receive only the resulting delta.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The trust boundary is important. Local pathfinding is not permission to dictate a position. It is a compact proposal. A modified client can invent waypoints, but the server can reject movement that conflicts with its own map or rules. Compute can move to the edge without moving authority with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tick turns time into a batch
&lt;/h2&gt;

&lt;p&gt;On each server cycle, the relevant work followed a simple order: read inbound packets, process players and queued actions, construct player updates, then flush outbound packets. NPCs, logins, and other systems added more stages, but the visible state transition retained that read-do-write shape.&lt;/p&gt;

&lt;p&gt;The tick does three useful things for the protocol.&lt;/p&gt;

&lt;p&gt;First, it gives the server a consistent boundary for resolving simultaneous actions. Second, it batches many small changes into one update per observer instead of emitting a new message for every property mutation. Third, it bounds how often unchanged state must be acknowledged.&lt;/p&gt;

&lt;p&gt;The price is equally real. A 600 ms quantum is perceptible. Modern action games often hide delay with prediction and reconciliation; RuneScape made the tick part of combat timing, skilling, and movement. Players learned its rhythm. Once game rules, animations, and content depend on that cadence, reducing it becomes a product redesign rather than a server setting.&lt;/p&gt;

&lt;p&gt;The lesson is not that slow ticks are universally good. It is that batching frequency belongs in the product model. If the experience can tolerate a coarser clock, the system gains a natural place to coalesce work. If it cannot, a compact wire format alone will not rescue responsiveness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every client keeps a mirror of nearby players
&lt;/h2&gt;

&lt;p&gt;The crucial bandwidth saving appears on the way back. A client does not wait for the server to resend complete player objects. It retains a list of nearby players with their last-known locations, appearances, animations, and chat state. The server’s update packet patches that mirror.&lt;/p&gt;

&lt;p&gt;This makes the most common message almost free: nothing changed.&lt;/p&gt;

&lt;p&gt;The composite player update begins in bit-access mode. For the local player, a single zero bit says there was no movement and no queued detail change. If the player walked, the record uses an update bit, a two-bit movement type, a three-bit direction, and a one-bit flag indicating whether a detailed block follows. Seven bits describe the step.&lt;/p&gt;

&lt;p&gt;The same approach scales across already known neighbors. The packet writes an eight-bit count and then, for each tracked player, one bit indicating whether anything changed. A static crowd of forty therefore costs 48 bits, or six bytes, before any uncommon detail blocks. Eight idle players fit in one byte rather than eight.&lt;/p&gt;

&lt;p&gt;This only works because ordering is shared state too. The server and client agree on which tracked player occupies each position in the list. A zero bit does not carry an identifier or schema. Its meaning comes from the client’s existing mirror and the fixed decoding procedure.&lt;/p&gt;

&lt;p&gt;Stateful protocols are powerful precisely because they do not repeat context. They are also operationally demanding. Lose the shared baseline, decode with a mismatched revision, or apply updates in the wrong order and the compact message becomes ambiguous. The bandwidth win creates obligations around sequencing, resynchronization, compatibility, and testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relative coordinates shrink the world to the viewport
&lt;/h2&gt;

&lt;p&gt;A player standing somewhere in the RuneScape world needs coordinates large enough to address thousands of tiles. Two 16-bit axes consume 32 bits. A newly visible player, however, cannot be thousands of tiles away. They are near the observer by definition.&lt;/p&gt;

&lt;p&gt;The protocol exploits that narrower domain. A new-player record uses an 11-bit player identifier, one detail flag, one teleport flag, and five signed bits for each coordinate offset. Five bits encode values from -16 through +15, enough for the visible neighborhood. Position falls from 32 bits to 10.&lt;/p&gt;

&lt;p&gt;The coordinate system is temporarily recentered on the observer. This is a broadly useful technique:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a map client can send movement relative to the current tile;&lt;/li&gt;
&lt;li&gt;a time-series codec can store deltas from the previous sample;&lt;/li&gt;
&lt;li&gt;an editor can describe an operation relative to the current document revision;&lt;/li&gt;
&lt;li&gt;a replicated simulation can encode entities relative to an area-of-interest origin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The saving comes from proving that the active domain is smaller than the global one. If an object can only be visible within 15 tiles, spending enough bits to place it anywhere on the planet is waste.&lt;/p&gt;

&lt;p&gt;The sentinel at player ID 2047 applies the same reasoning to list termination. One otherwise unused value means “no more players,” so the stream does not need a separate length field for that section. Good binary formats repeatedly look for values the domain cannot produce and assign them protocol meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  One packet, two representations
&lt;/h2&gt;

&lt;p&gt;It would be tempting to bit-pack everything after seeing these gains. RuneScape did not. After local movement, tracked neighbors, and new arrivals are decoded, the reader aligns to the next byte and processes detailed updates conventionally.&lt;/p&gt;

&lt;p&gt;Those blocks can contain facing direction, animation, chat, hits, graphical effects, forced movement, equipment, colors, combat level, and other appearance data. An appearance record alone can occupy roughly 44 to 80 bytes. A flag byte says which categories are present. A marker in that first byte buys a second flag byte only when one of the rarer high flags is needed.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Frunescape-56k-protocol%2Fplayer-update-layers.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Frunescape-56k-protocol%2Fplayer-update-layers.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The packet changes representation where the scarce resource changes from downstream bytes to server-side assembly work.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Why stop packing when the records become larger? Because size alone does not determine whether bit packing pays.&lt;/p&gt;

&lt;p&gt;The early fields have enormous repeated slack. “No change” needs one bit but would consume a whole byte in a byte-oriented structure, and it occurs for many players every cycle. Movement direction has eight possibilities, so three bits are sufficient. The saving repeats across the whole visible crowd.&lt;/p&gt;

&lt;p&gt;Detailed records have a different shape. A target ID already occupies a complete short. An animation ID and delay are real fields rather than tiny defaults. More importantly, these blocks are uncommon because the earlier bits gate them. There is no crowd-sized multiplier.&lt;/p&gt;

&lt;p&gt;Byte alignment also enables reuse. The client caches appearance buffers, and a server can construct a changed appearance once and splice the same bytes into updates for multiple observers. A byte-aligned blob is position independent. If it started at an arbitrary bit offset, every insertion could require shifting and masking based on everything written before it. A few saved wire bits would consume more server CPU across a large fan-out.&lt;/p&gt;

&lt;p&gt;The packet therefore optimizes two resources in one message. Its front half spends modest compute to protect dial-up bandwidth. Its back half preserves alignment so expensive detail work can be cached and copied.&lt;/p&gt;

&lt;h2&gt;
  
  
  The opcode cipher protected structure, not content
&lt;/h2&gt;

&lt;p&gt;After login, packet opcodes passed through ISAAC, Bob Jenkins’s stream-oriented pseudorandom design. The client and server maintained separate streams for each direction, seeded from shared values with a small transformation separating the inbound and outbound sequences.&lt;/p&gt;

&lt;p&gt;Only the opcode was enciphered. The body was not confidential. This was an obstacle for packet parsers and unofficial clients, not modern transport security. Without the decoded opcode, a reader did not know which packet schema or length rule to apply, so the remaining bytes were harder to frame. Revisions also shuffled field order, changed endianness, negated bytes, and added constants as anti-cheat friction.&lt;/p&gt;

&lt;p&gt;That distinction deserves precision. Obfuscating message boundaries can raise the cost of reverse engineering, but it does not provide the authenticity, integrity, and confidentiality expected from TLS. The &lt;a href="https://github.com/2004Scape/Client2" rel="noopener noreferrer"&gt;2004Scape Client2 project&lt;/a&gt; exists because determined preservationists could source-port the original Java behavior despite those measures. Its maintainers are explicit that their work is a port from recovered client behavior, not leaked Jagex source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared knowledge is the real compression dictionary
&lt;/h2&gt;

&lt;p&gt;The protocol is small because the two programs were designed as halves of one runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both have the collision map, so paths can be sent as corners and checked independently.&lt;/li&gt;
&lt;li&gt;Both retain the visible-player list, so a one-bit flag can modify an implied record.&lt;/li&gt;
&lt;li&gt;Both understand the viewport, so local offsets replace global coordinates.&lt;/li&gt;
&lt;li&gt;Both compile the same update-field table, so a bitmask replaces a self-describing schema.&lt;/li&gt;
&lt;li&gt;Both follow the same tick order, so many events can become one deterministic state delta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;General-purpose APIs usually carry more description because their clients evolve independently. Field names, type tags, resource identifiers, version negotiation, and explicit lengths buy flexibility. RuneScape traded much of that flexibility for density.&lt;/p&gt;

&lt;p&gt;That trade was rational for a first-party game client shipped alongside its server. It would be dangerous to copy blindly into a public platform. A tightly coupled binary protocol requires coordinated rollout, strong golden tests, careful revision handling, and a recovery path when mirrors disagree. An undocumented client assumption is not free merely because it does not cross the wire.&lt;/p&gt;

&lt;p&gt;The preservation story makes this cost visible. Early game versions are incomplete historical artifacts. The &lt;a href="https://github.com/2004Scape/ServerOld" rel="noopener noreferrer"&gt;2004Scape server project&lt;/a&gt; describes itself as an emulator reconstructed without original server source, while the &lt;a href="https://rs-archive.github.io/" rel="noopener noreferrer"&gt;RuneScape Archive Project&lt;/a&gt; collects surviving clients and caches. When a protocol’s schema lives mostly in two matching binaries, losing either side also loses documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the design today
&lt;/h2&gt;

&lt;p&gt;Modern bandwidth is larger, but the multipliers are larger too. Multiplayer simulations, collaborative editors, market feeds, IoT fleets, and mobile applications still pay for fan-out, battery, radio wakeups, serialization, and server assembly. The useful principles survive the modem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model the common case first.&lt;/strong&gt; Measure what happens most frequently, then make that state cheap. If 98 percent of entities do not change during an interval, optimize the unchanged marker before compressing the rare full record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrow the domain before choosing a type.&lt;/strong&gt; Global identifiers and coordinates may be unnecessary inside a scoped list, viewport, shard, or session. Prove the local bounds and encode those.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send decisions, not derivable steps.&lt;/strong&gt; When both ends have deterministic inputs, transmit route corners, operation parameters, or a compact seed rather than the entire derived result. Keep validation authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch on a product boundary.&lt;/strong&gt; A tick, animation frame, transaction, or document revision can turn scattered mutations into one ordered delta. Choose the cadence from user experience, not only infrastructure convenience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch formats when the economics change.&lt;/strong&gt; Bit packing is valuable where slack is small and repeated. Byte alignment, fixed records, or a conventional serializer may win where fields are large, rare, cached, or frequently inspected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget server fan-out, not only packet size.&lt;/strong&gt; A representation that saves two bytes but forces a custom bit shift for every observer may lose at scale. Count encoding work, allocations, cache reuse, and copies alongside wire bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design resynchronization before launch.&lt;/strong&gt; Stateful deltas need sequence numbers, snapshots, revision negotiation, or another way to rebuild the baseline. The smaller the incremental message, the more it depends on that baseline being correct.&lt;/p&gt;

&lt;p&gt;RuneScape’s elegant result was not “seven bytes” by itself. It was a system in which seven bytes were sufficient because computation, authority, visibility, order, and cached state had already been placed deliberately. The modem forced discipline, but the enduring lesson is architectural: the cheapest fact to transmit is the one both sides can safely derive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://jkm.dev/posts/how-2004-runescape-fit-a-multiplayer-rpg-into-56k-dialup/" rel="noopener noreferrer"&gt;How 2004 RuneScape fit a multiplayer RPG into 56k dial-up&lt;/a&gt;, James Monger’s detailed walk through the decompiled client behavior.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://news.ycombinator.com/item?id=49516699" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt;, where the article reached 134 points and 83 comments on the September 1, 2026 front page.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/2004Scape/Client2" rel="noopener noreferrer"&gt;2004Scape Client2&lt;/a&gt;, a TypeScript source port of the May 18, 2004 client.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/2004Scape/ServerOld" rel="noopener noreferrer"&gt;2004Scape Server&lt;/a&gt;, an independently written emulator focused on reproducing the May 2004 experience.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/javase/tutorial/deployment/applet/security.html" rel="noopener noreferrer"&gt;What applets can and cannot do&lt;/a&gt;, Oracle’s archived guide to the Java applet sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.burtleburtle.net/bob/rand/isaacafa.html" rel="noopener noreferrer"&gt;ISAAC&lt;/a&gt;, Bob Jenkins’s original description and implementation notes.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>How Autocomplete Searches 240 Million Domains Before Keyup</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/how-autocomplete-searches-240-million-domains-before-keyup-3nn1</link>
      <guid>https://dev.to/0xgosu/how-autocomplete-searches-240-million-domains-before-keyup-3nn1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3stjr6smyqzu4jyijuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3stjr6smyqzu4jyijuk.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fastest request is not always the one that finishes first. Sometimes it is the one that begins before the user starts waiting.&lt;/p&gt;

&lt;p&gt;Wirewiki’s domain autocomplete is built around that distinction. Its index covers roughly 240 million names, the application runs on one server in Europe, and every uncached request travels through Cloudflare, Nginx, and an API. Yet for the system’s author, suggestions were ready before key release at the 99th percentile of his typing sample.&lt;/p&gt;

&lt;p&gt;The headline number is “0 ms,” but no packet crosses a network in zero time. The measurement starts at &lt;code&gt;keyup&lt;/code&gt;, when the interface owes the user a visible answer. Work begins earlier, at &lt;code&gt;keydown&lt;/code&gt;, and the response includes not only matches for the current prefix but prepared matches for every possible next character. When the next key arrives, the browser usually selects an answer it already has.&lt;/p&gt;

&lt;p&gt;That is the first half of the design: move work into time the user is already spending. The second half makes the speculation affordable. A small, popular “head” stays in an in-memory trie. A much larger “tail” lives in delta-compressed, memory-mapped blocks on SSD. The API searches the head first, touches the tail only when needed, and returns the eight most useful names.&lt;/p&gt;

&lt;p&gt;Together, those choices form a practical lesson in perceived performance. Make the deadline explicit, predict a bounded amount of likely future work, and give each part of the data set a representation that matches how often it is touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero milliseconds is a product definition
&lt;/h2&gt;

&lt;p&gt;Latency needs two timestamps. Backend dashboards usually choose request received and response sent. Network tools measure round-trip time. A person using autocomplete experiences a different interval: the time between completing an input action and seeing the interface react.&lt;/p&gt;

&lt;p&gt;For this design, the clock begins when a key is released. If suggestions are ready by then, measured wait is zero even though the browser, network, and server have already done real work.&lt;/p&gt;

&lt;p&gt;The browser starts a request on &lt;code&gt;keydown&lt;/code&gt;. Suppose the current text becomes &lt;code&gt;wi&lt;/code&gt;. The API returns the best matches for &lt;code&gt;wi&lt;/code&gt;, plus separate result lists for &lt;code&gt;wi-&lt;/code&gt;, &lt;code&gt;wi.&lt;/code&gt;, &lt;code&gt;wi0&lt;/code&gt; through &lt;code&gt;wi9&lt;/code&gt;, and &lt;code&gt;wia&lt;/code&gt; through &lt;code&gt;wiz&lt;/code&gt;. If the next key is &lt;code&gt;k&lt;/code&gt;, the browser can take the prepared &lt;code&gt;wik&lt;/code&gt; branch from its cache instead of waiting for another round trip.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdomain-autocomplete-240m%2Ftyping-latency-budget.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdomain-autocomplete-240m%2Ftyping-latency-budget.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The network is not instantaneous. It completes inside the interval between early input events and the moment the interface must paint.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event" rel="noopener noreferrer"&gt;browser &lt;code&gt;keyup&lt;/code&gt; event&lt;/a&gt; is fired when a key is released. That makes the measurement concrete, but it is not universal human truth. Touch keyboards, paste, speech input, input method editors, accessibility tools, and programmatic changes do not all follow the same simple physical-key sequence. A production implementation still needs an &lt;code&gt;input&lt;/code&gt;-driven correctness path. Key events are an optimization opportunity, not the only source of state.&lt;/p&gt;

&lt;p&gt;The author’s test involved typing 100 domain names at a reasonably fast pace. The 99th-percentile interval from one key press to the next release was 121 ms. That became the working end-to-end budget.&lt;/p&gt;

&lt;p&gt;At 60 Hz, a display produces a new frame every 16.7 ms. Rendering within the next frame can add a little scheduling slack, but not much at the tail. The important number is therefore not an abstract “fast API” target. It is: &lt;strong&gt;can the complete path finish before the next key release for almost every measured interaction?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Speculation is cheap when the future is bounded
&lt;/h2&gt;

&lt;p&gt;Prefetching can easily become waste. Search systems with a large alphabet, rich filters, or long result objects might multiply bandwidth by dozens of branches that are never used. Domain autocomplete has unusually helpful constraints.&lt;/p&gt;

&lt;p&gt;Wirewiki uses 38 next characters: &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, hyphen, and dot. With eight suggestions for the current prefix and eight for each possible continuation, one response contains at most 312 domain names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(38 possible next characters + current prefix) × 8 suggestions = 312 names

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uncompressed response is at most about 5 KB in the author’s measurements and roughly 2.5 KB over the wire after compression. This is deliberate overfetching, but it is capped. The client exchanges a few kilobytes for the chance to remove an entire future round trip.&lt;/p&gt;

&lt;p&gt;That trade works because the response contains small strings, the branch factor is fixed, and typing provides immediate evidence about which branch will be used. Apply the same pattern to product cards with images and it could be ruinous. The general rule is not “prefetch everything.” It is &lt;strong&gt;prefetch a small, bounded set whose cost is lower than the delay it can hide&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Client caching also needs discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache entries must be keyed by the exact normalized prefix and query options.&lt;/li&gt;
&lt;li&gt;Stale requests should be cancelled with &lt;code&gt;AbortController&lt;/code&gt; or ignored by sequence number, so an older response cannot replace newer suggestions.&lt;/li&gt;
&lt;li&gt;Backspace can often reuse a parent prefix already held in memory.&lt;/li&gt;
&lt;li&gt;Composition events need special handling so an incomplete IME sequence is not treated as committed input.&lt;/li&gt;
&lt;li&gt;Result lists should remain accessible with correct combobox semantics, keyboard selection, and screen-reader announcements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speculation changes when work happens. It must not change which input wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Popularity and completeness need different structures
&lt;/h2&gt;

&lt;p&gt;A single data structure is rarely ideal for both the most popular million domains and the other hundreds of millions. The first group is small enough to keep hot and important enough to deserve the fastest path. The second group provides completeness, but most names will rarely be requested.&lt;/p&gt;

&lt;p&gt;Wirewiki builds its popular head from &lt;a href="https://tranco-list.eu/" rel="noopener noreferrer"&gt;Tranco&lt;/a&gt;, a research-oriented ranking of one million domains. Tranco combines multiple source lists over a 30-day window to improve stability and resistance to manipulation; its &lt;a href="https://tranco-list.eu/methodology" rel="noopener noreferrer"&gt;methodology&lt;/a&gt; also documents the biases in each underlying source. That makes it more useful than treating any one traffic list as an unquestionable measure of popularity.&lt;/p&gt;

&lt;p&gt;The long tail comes mainly from generic top-level-domain zone files. ICANN’s &lt;a href="https://www.icann.org/en/contracted-parties/registry-operators/services/centralized-zone-data-service" rel="noopener noreferrer"&gt;Centralized Zone Data Service&lt;/a&gt; provides a common place to request zone files from participating registries. It covers many gTLDs such as &lt;code&gt;.com&lt;/code&gt;, &lt;code&gt;.net&lt;/code&gt;, and &lt;code&gt;.org&lt;/code&gt;, but it does not provide the same broad access to country-code TLDs. Popular ccTLD names still appear through Tranco, while certificate-transparency logs and web archives are possible future sources for broader coverage.&lt;/p&gt;

&lt;p&gt;This data split is semantic before it is technical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The head answers “what are people most likely trying to reach?”&lt;/li&gt;
&lt;li&gt;The tail answers “does a matching active name exist even if it is obscure?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Autocomplete needs both. A popularity-only index feels fast but incomplete. An alphabetical list of everything can return obscure names ahead of the destination a person probably intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  The head: precompute the answer at every prefix
&lt;/h2&gt;

&lt;p&gt;The top one million names live in a character trie. Each edge represents the next character and each node represents a prefix. A lookup walks the characters the user typed, so its cost grows with the query length rather than the total number of domains.&lt;/p&gt;

&lt;p&gt;The more important optimization is stored at the nodes: every prefix already has its top eight suggestions. A request for &lt;code&gt;wik&lt;/code&gt; does not descend through all matching names, gather candidates, and sort them on demand. It reaches the &lt;code&gt;wik&lt;/code&gt; node and reads the answer prepared when the index was built.&lt;/p&gt;

&lt;p&gt;That shifts work from request time to build time and trades memory for predictable latency. It is a strong fit because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The popular set changes much more slowly than users type.&lt;/li&gt;
&lt;li&gt;Eight results are enough for the interface.&lt;/li&gt;
&lt;li&gt;Every query needs the same operation: retrieve the best completions for a prefix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complexity is often written as &lt;code&gt;O(length of prefix)&lt;/code&gt;. More usefully, the request path is a short chain of dependent memory reads with no scan proportional to the million-name corpus and no per-request ranking pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tail: a small directory over compressed blocks
&lt;/h2&gt;

&lt;p&gt;Keeping 240 million strings in ordinary heap objects would spend heavily on pointers, allocation metadata, string headers, and repeated prefixes. The tail instead uses a sorted, delta-compressed file divided into fixed blocks of 256 names.&lt;/p&gt;

&lt;p&gt;A 27 MB in-memory directory identifies the block that may contain a prefix. The API binary-searches that directory, then scans one small block. Because adjacent sorted domain names tend to share leading characters, delta compression stores the common prefix once and records only the changing suffix for later entries.&lt;/p&gt;

&lt;p&gt;The complete 240-million-name data set occupies about 2.5 GB on disk in the author’s implementation. The file is memory-mapped, so the program can address it like memory while the operating system loads pages on demand. Linux readahead brings file content into the page cache before every byte is explicitly requested, and it avoids repeating I/O for pages already present; the &lt;a href="https://www.kernel.org/doc/html/latest/core-api/mm-api.html#readahead" rel="noopener noreferrer"&gt;kernel’s memory-management documentation&lt;/a&gt; describes that mechanism. Hot tail blocks naturally remain resident, while cold blocks cost SSD access only when needed.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdomain-autocomplete-240m%2Fhead-tail-index.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdomain-autocomplete-240m%2Fhead-tail-index.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The head spends RAM on predictable popular lookups. The tail spends a small directory, compact SSD storage, and page-cache locality on completeness.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The tail lookup is described as &lt;code&gt;O(length of prefix × log number of domains)&lt;/code&gt;, followed by a bounded block scan. Both query length and corpus size are capped in the deployed system, so the practical goal is not an asymptotic breakthrough. It is a narrow worst-case path: one directory search, one block, and at most 256 decoded names.&lt;/p&gt;

&lt;p&gt;Fixed-size blocks are the quiet hero. A huge compressed stream might achieve a slightly better ratio but require decoding from a distant checkpoint. Very small blocks add directory overhead. A block of 256 names limits work and gives the page cache a useful chunk to retain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search the head first, use the tail to fill gaps
&lt;/h2&gt;

&lt;p&gt;The two indexes are not peers. Popular results should always win. The API searches the in-memory trie first and consults the tail only when the head cannot fill the result list. It then deduplicates and preserves rank order.&lt;/p&gt;

&lt;p&gt;That order encodes product policy directly into the query plan. A popular domain that matches a prefix must not be displaced by an alphabetically earlier but unknown registration. The head provides relevance; the tail provides recall.&lt;/p&gt;

&lt;p&gt;There is a broader pattern here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Optimized for&lt;/th&gt;
&lt;th&gt;Representation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Next interaction&lt;/td&gt;
&lt;td&gt;Small speculative prefix cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Popular head&lt;/td&gt;
&lt;td&gt;Relevance and latency&lt;/td&gt;
&lt;td&gt;In-memory trie with precomputed top eight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long tail&lt;/td&gt;
&lt;td&gt;Coverage and density&lt;/td&gt;
&lt;td&gt;Sorted delta-compressed blocks on SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating system&lt;/td&gt;
&lt;td&gt;Locality&lt;/td&gt;
&lt;td&gt;Memory mapping, page cache, and readahead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each layer predicts at a different horizon. The browser predicts the next character. The trie predicts which names matter for a prefix. The page cache predicts which nearby disk pages will be touched again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API became cheaper than the network
&lt;/h2&gt;

&lt;p&gt;The author generated 720,000 keystroke queries from 60,000 simulated domain-name typing sessions and replayed them open-loop: requests were fired at a fixed rate rather than waiting for earlier responses. That matters because closed-loop load tests can accidentally reduce pressure when the server slows down.&lt;/p&gt;

&lt;p&gt;Most API-only requests completed within 2 ms. At 1,600 requests per second, Nginx plus the API stayed within 15 ms at p99 in the reported production-server test. The benchmark also exercised the end-to-end route through Cloudflare.&lt;/p&gt;

&lt;p&gt;Those are the author’s measurements, not an independent benchmark, and the workload is shaped like his own product. Still, the ordering of costs is instructive. Once index lookup falls to a few milliseconds, the network dominates. Further micro-optimization at the origin cannot recover 100 ms of geographic round-trip time.&lt;/p&gt;

&lt;p&gt;In practice, end-to-end autocomplete latency was roughly the browser-to-origin round trip through Cloudflare plus another 10 ms. Cloudflare can absorb repeated traffic and cache hot requests, but it cannot repeal distance for a miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asterisk is geography
&lt;/h2&gt;

&lt;p&gt;The single origin is in Europe. For users in the United States, the author observed an additional 100–200 ms—enough to exceed his 121 ms p99 typing budget. The system can feel instant locally without delivering the same tail latency globally.&lt;/p&gt;

&lt;p&gt;Multiple regional replicas plus geographic routing would reduce that distance. The read-only index is a friendly replication workload: build an immutable artifact, distribute it, and swap versions atomically. The hard parts would be operational rather than algorithmic—deployment, health checks, routing, observability, cache behavior, and keeping measurements comparable across regions.&lt;/p&gt;

&lt;p&gt;Wirewiki stops before that complexity because autocomplete is a feature, not currently a standalone business. This is sound engineering restraint. The goal is not the lowest number a distributed system could possibly achieve. The goal is the best experience worth operating for the product that exists.&lt;/p&gt;

&lt;p&gt;The asterisk therefore improves the lesson. “P99 0 ms” is true for a specific definition, client behavior, typing sample, and geography. Performance claims become useful when their boundaries are explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable design process
&lt;/h2&gt;

&lt;p&gt;This system can be reduced to six decisions that apply beyond domain search.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Define the human deadline
&lt;/h3&gt;

&lt;p&gt;Choose the event after which delay becomes visible. It may be key release, pointer release, viewport entry, or the opening of a panel. Measure from that boundary, while still recording real end-to-end time separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Look for work that can begin earlier
&lt;/h3&gt;

&lt;p&gt;Hover, focus, keydown, partial input, and navigation intent can create safe speculation windows. Start only work that remains correct if the prediction is wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bound the speculative bill
&lt;/h3&gt;

&lt;p&gt;Estimate branch count, response bytes, server work, cancellation rate, and cache reuse. Prefetching is a budget, not a ritual.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Split data by access frequency
&lt;/h3&gt;

&lt;p&gt;Do not force the hot million and cold hundreds of millions through the same representation. Give the head more memory and precomputation; give the tail density and bounded access.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Make the slow path narrow
&lt;/h3&gt;

&lt;p&gt;A tail lookup should touch one small directory and one small block, not wake an unbounded scan. Fixed work is easier to reason about at p99.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Stop when another layer dominates
&lt;/h3&gt;

&lt;p&gt;When network distance costs two orders of magnitude more than lookup, optimize placement or accept the boundary. Shaving microseconds from the trie is no longer the highest-value move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instant interfaces are scheduled, not magical
&lt;/h2&gt;

&lt;p&gt;This autocomplete does not defeat latency. It schedules around it.&lt;/p&gt;

&lt;p&gt;The browser borrows time from the physical act of typing. The response speculates over a fixed 38-character future. The in-memory trie makes popular queries a short prefix walk. The compressed block index keeps 240 million names affordable. Memory mapping lets the operating system decide which tail pages deserve RAM. Finally, the product accepts that one European server cannot deliver the same p99 experience everywhere.&lt;/p&gt;

&lt;p&gt;That stack is more interesting than any single data structure. Perceived speed emerges when client timing, payload shape, ranking policy, storage layout, operating-system behavior, and geography agree on the same deadline.&lt;/p&gt;

&lt;p&gt;The useful question is not “how can this API return in zero milliseconds?” It is: &lt;strong&gt;what must already be true when the user expects the next frame?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruurtjan.com/articles/p99-0ms-autocomplete-for-240-million-domain-names" rel="noopener noreferrer"&gt;Ruurtjan Pul: p99 0 ms autocomplete for 240 million domain names&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tranco-list.eu/" rel="noopener noreferrer"&gt;Tranco: research-oriented top-sites ranking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tranco-list.eu/methodology" rel="noopener noreferrer"&gt;Tranco ranking methodology&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.icann.org/en/contracted-parties/registry-operators/services/centralized-zone-data-service" rel="noopener noreferrer"&gt;ICANN: Centralized Zone Data Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/core-api/mm-api.html#readahead" rel="noopener noreferrer"&gt;Linux kernel documentation: readahead and the page cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event" rel="noopener noreferrer"&gt;MDN: &lt;code&gt;keyup&lt;/code&gt; event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nngroup.com/articles/response-times-3-important-limits/" rel="noopener noreferrer"&gt;Nielsen Norman Group: response-time limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>What CPython's Official RISC-V Support Actually Guarantees</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Mon, 31 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/what-cpythons-official-risc-v-support-actually-guarantees-5g30</link>
      <guid>https://dev.to/0xgosu/what-cpythons-official-risc-v-support-actually-guarantees-5g30</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddpm9pomd9c1hefapl8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddpm9pomd9c1hefapl8b.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Python could run on RISC-V before August 2026. Linux distributions shipped it, developers built it, and architecture-specific patches had been landing for years. What changed when CPython called RISC-V “officially supported” was not the sudden appearance of a working interpreter. The change was a maintenance contract.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;riscv64-unknown-linux-gnu&lt;/code&gt; target now appears in &lt;a href="https://peps.python.org/pep-0011/" rel="noopener noreferrer"&gt;PEP 11&lt;/a&gt; as a Tier 3 platform for both GCC and Clang with glibc. It has reliable buildbots and named maintainers. That means the core project continuously checks the platform and has people who have agreed to look after it. It does &lt;strong&gt;not&lt;/strong&gt; yet mean that every regression blocks a merge or a Python release.&lt;/p&gt;

&lt;p&gt;That distinction is the useful story. Architecture support in a large runtime is not a Boolean. It is a chain running from instruction-set definitions through compilers, operating systems, interpreter internals, continuous testing, binary packaging, and real applications. CPython has now made one important link durable. The rest of the Python ecosystem still has work to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  “It builds” is not the same as “we support it”
&lt;/h2&gt;

&lt;p&gt;A portable C program often reaches a new architecture surprisingly early. A compiler can translate most of the code, the operating system supplies familiar POSIX interfaces, and a developer can patch the handful of assumptions that fail. That produces a successful build. It does not ensure the next change to garbage collection, atomics, stack unwinding, &lt;code&gt;libffi&lt;/code&gt;, profiling, or the experimental JIT will preserve that success.&lt;/p&gt;

&lt;p&gt;PEP 11 exists because dormant platform code becomes a liability. Without machines that run the test suite and maintainers who understand the failures, the main branch can drift until the port is broken. Nobody knows whether an old conditional is still required, and ordinary refactoring may accidentally depend on x86-64 or AArch64 behavior.&lt;/p&gt;

&lt;p&gt;Tiered support replaces the vague claim “Python runs here” with explicit obligations.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcpython-riscv-tier3%2Fsupport-tier-contracts.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcpython-riscv-tier3%2Fsupport-tier-contracts.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;RISC-V has crossed into a maintained Tier 3 contract; Tier 2 is the next meaningful threshold.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;Tier 3&lt;/strong&gt; , a platform needs a reliable buildbot and at least one core developer as a contact. Failures do not block releases, and PEP 11 specifies no response deadline. This is where 64-bit RISC-V now sits, with Stan Ulbrych and Emma Smith listed for the GCC and Clang configurations.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;Tier 2&lt;/strong&gt; , a platform still needs reliable automation but must have at least two core maintainers. A break must be fixed or reverted within 24 hours, and a failing platform blocks a release. AArch64 Linux with Clang, Windows on ARM64, WASI, x86-64 macOS, and x86-64 Linux with Clang currently occupy this tier.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;Tier 1&lt;/strong&gt; , failures block releases and breaking changes may not remain on &lt;code&gt;main&lt;/code&gt;. Responsibility belongs to all core developers, not only named platform specialists. The list covers the dominant release environments, including x86-64 Linux with GCC, AArch64 Linux with GCC, macOS on Apple silicon, and the principal Windows targets.&lt;/p&gt;

&lt;p&gt;The tiers are not a score of processor quality. They describe how much project-wide disruption CPython is willing to accept to preserve a target. RISC-V entering Tier 3 means the port is no longer merely tolerated platform code. It has a documented place in CPython’s development process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why real RISC-V machines changed the equation
&lt;/h2&gt;

&lt;p&gt;The decisive resource was not one more portability patch. It was dependable hardware.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://blog.python.org/2026/08/riscv-now-officially-supported/" rel="noopener noreferrer"&gt;Python announcement&lt;/a&gt; credits the RISE Project with providing several RISC-V machines for buildbots and architecture-specific debugging. Those systems repeatedly build the current branch and run CPython’s test suite after changes land. A regression that would otherwise wait for a user report becomes visible to maintainers.&lt;/p&gt;

&lt;p&gt;Cross-compilation cannot provide the same feedback. It proves that a compiler can emit a binary for another target; it does not naturally execute the full suite on that target. Emulation is excellent for bootstrapping and broad test matrices, but it can hide timing behavior, differ around kernel and hardware features, and make an already large test suite much slower. Native machines expose the software stack that users actually run.&lt;/p&gt;

&lt;p&gt;RISC-V makes this especially important because it is a family of implementations, not a single chip. The &lt;a href="https://docs.riscv.org/reference/isa/v20250508/unpriv/intro.html" rel="noopener noreferrer"&gt;unprivileged ISA specification&lt;/a&gt; defines a small base instruction set plus optional extensions. RV64I supplies the 64-bit integer base; common systems add multiplication and division, atomics, floating point, compressed instructions, vectors, and other extensions. Implementations can differ in microarchitecture and available extensions while presenting the same broad architecture name.&lt;/p&gt;

&lt;p&gt;A runtime can be perfectly ordinary C at its center and still touch machine-specific seams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Atomic operations and memory-ordering assumptions become critical in a free-threaded interpreter.&lt;/li&gt;
&lt;li&gt;Stack walking, signal handling, and profilers depend on calling conventions and register definitions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ctypes&lt;/code&gt; and extension modules depend on ABI details and foreign-function libraries.&lt;/li&gt;
&lt;li&gt;Dynamic code generation needs a machine-code backend, instruction-cache synchronization, and unwind metadata.&lt;/li&gt;
&lt;li&gt;Compiler optimizations can expose alignment or undefined-behavior bugs that another architecture never revealed.&lt;/li&gt;
&lt;li&gt;Tests involving timeouts or concurrency can behave differently on slower cores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One historical example is &lt;a href="https://github.com/python/cpython/issues/121138" rel="noopener noreferrer"&gt;CPython issue #121138&lt;/a&gt;, where enabling Linux &lt;code&gt;perf&lt;/code&gt; trampoline support caused a RISC-V build to stop at an explicit “unsupported target architecture” error. The core interpreter was not fundamentally incapable of running. A specialized observability feature had reached an architecture-specific boundary. That is typical porting work: the last few percent lives in debugging, packaging, performance, and operational tools rather than the language evaluator alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feedback loop still starts too late
&lt;/h2&gt;

&lt;p&gt;Buildbots give CPython the evidence required for Tier 3, but their timing matters. They generally test after a patch has merged. If a change breaks RISC-V, the main branch can remain broken until the builder reports the result and someone prepares a repair.&lt;/p&gt;

&lt;p&gt;The next infrastructure step is to add RISC-V to pre-merge continuous integration. RISE’s runner service provides ephemeral Ubuntu jobs on native RISC-V hardware. A workflow can select &lt;code&gt;ubuntu-24.04-riscv&lt;/code&gt;, compile the proposed revision, run tests, and discard the machine after the job. That gives the author feedback while the patch is still under review.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcpython-riscv-tier3%2Friscv-testing-feedback-loop.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fcpython-riscv-tier3%2Friscv-testing-feedback-loop.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;Buildbots establish continuous support; native pre-merge runners shorten the time between introducing and seeing a regression.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;This is more than a speed improvement. Earlier feedback changes who can repair a problem. The author still has the patch in context and can amend it before unrelated changes accumulate. Reviewers can see the platform result beside the existing checks. Maintainers spend less time bisecting a failure that appeared after merge.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://riscv-runners.riseproject.dev/docs/getting-started/configure" rel="noopener noreferrer"&gt;RISE runner documentation&lt;/a&gt; describes an environment meant to resemble a standard GitHub Actions Ubuntu runner, with native GCC and Clang, containers, and common language toolchains. It is not a substitute for CPython’s long-lived buildbot fleet. It complements that fleet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull-request CI protects the merge boundary.&lt;/li&gt;
&lt;li&gt;Buildbots continuously exercise configurations, slow tests, and post-merge state.&lt;/li&gt;
&lt;li&gt;Maintainers investigate architecture-specific failures.&lt;/li&gt;
&lt;li&gt;Release policy determines whether a failure is advisory or blocking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Tier 2, all four parts must work reliably enough to support a 24-hour repair expectation and release-blocking status. Hardware availability is necessary, but the social commitment to respond is equally important.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Python runtime is only the first layer
&lt;/h2&gt;

&lt;p&gt;Installing pure-Python packages on RISC-V is usually uneventful: source files and bytecode are architecture-independent. The friction begins with native extensions.&lt;/p&gt;

&lt;p&gt;Packages such as NumPy, cryptography libraries, image codecs, database drivers, and machine-learning frameworks often publish wheels containing compiled code. On a common x86-64 system, &lt;code&gt;pip install&lt;/code&gt; downloads a matching wheel in seconds. When a project does not publish a RISC-V wheel, &lt;code&gt;pip&lt;/code&gt; falls back to a source build or reports that no compatible distribution exists. The user then needs compilers, headers, system libraries, memory, time, and a build process that already understands the architecture.&lt;/p&gt;

&lt;p&gt;The packaging foundation is improving. The &lt;a href="https://github.com/pypa/manylinux" rel="noopener noreferrer"&gt;manylinux project&lt;/a&gt; now lists an alpha &lt;code&gt;manylinux_2_39_riscv64&lt;/code&gt; image and a &lt;code&gt;musllinux_1_2_riscv64&lt;/code&gt; image. Its containers include multiple CPython versions and the tooling needed to produce policy-compliant wheels. &lt;a href="https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/build-platforms.toml" rel="noopener noreferrer"&gt;cibuildwheel’s platform matrix&lt;/a&gt; contains RISC-V CPython build identifiers. The standardized &lt;code&gt;manylinux_x_y_arch&lt;/code&gt; scheme lets a wheel declare the minimum glibc generation it expects rather than tying it to one distribution.&lt;/p&gt;

&lt;p&gt;Those capabilities make publishing possible; they do not make it automatic. Every package with native code must decide to add the architecture, obtain native or emulated CI capacity, repair its dependencies, and upload artifacts. A scientific application may depend on dozens of compiled packages, so one missing wheel can push the entire installation down the source-build path.&lt;/p&gt;

&lt;p&gt;This creates a practical support ladder for users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interpreter:&lt;/strong&gt; Does CPython build and pass its own tests?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging tools:&lt;/strong&gt; Can &lt;code&gt;pip&lt;/code&gt;, wheel tags, audit tools, and build images represent the target?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foundational libraries:&lt;/strong&gt; Are compilers, BLAS, OpenSSL, libffi, Rust, and other native dependencies healthy?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Published wheels:&lt;/strong&gt; Do maintainers continuously upload RISC-V artifacts?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applications:&lt;/strong&gt; Do real workloads pass their own suites and meet performance expectations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CPython’s Tier 3 milestone stabilizes the bottom of that ladder. It gives upstream package maintainers a more trustworthy runtime to target and makes failures easier to classify. It cannot promise that a random &lt;code&gt;requirements.txt&lt;/code&gt; will install without compilation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers can test today
&lt;/h2&gt;

&lt;p&gt;The highest-value reports come from ordinary workloads, not only from rebuilding CPython.&lt;/p&gt;

&lt;p&gt;On a 64-bit RISC-V Linux machine, developers can build the current CPython branch with the usual &lt;code&gt;configure&lt;/code&gt; and &lt;code&gt;make&lt;/code&gt; flow, run &lt;code&gt;make test&lt;/code&gt;, and then exercise real projects. Testing with both GCC and Clang matters because PEP 11 lists separate configurations. So does recording the distribution, kernel, compiler, libc, CPU, and available ISA extensions. “Fails on RISC-V” is far less actionable than a reproducible command with a precise environment.&lt;/p&gt;

&lt;p&gt;Package maintainers can add a native runner job without making it release-blocking on day one. Start with a scheduled or allowed-to-fail build, measure its duration and reliability, then promote it once failures are understood. For a C or Rust extension, verify more than import success: run the full suite, build a wheel, inspect its platform tag, install it into a clean environment, and test the installed artifact.&lt;/p&gt;

&lt;p&gt;Performance work should follow correctness. RISC-V’s modular extensions create room for architecture-specific optimization, but a baseline binary must remain valid on the systems it claims to support. Dispatching to vector or crypto extensions requires feature detection and a safe fallback. Assuming that every &lt;code&gt;riscv64&lt;/code&gt; machine exposes the same extension set would trade portability for a benchmark win.&lt;/p&gt;

&lt;p&gt;The most useful bug reports will cluster around boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;free-threaded builds and atomics;&lt;/li&gt;
&lt;li&gt;native extension compilation and linking;&lt;/li&gt;
&lt;li&gt;profilers, debuggers, and stack unwinding;&lt;/li&gt;
&lt;li&gt;JIT or executable-memory behavior;&lt;/li&gt;
&lt;li&gt;wheel production and repair;&lt;/li&gt;
&lt;li&gt;numerical libraries and vectorized kernels;&lt;/li&gt;
&lt;li&gt;tests that are sensitive to timing or memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reporting these upstream matters because Tier 3 has no automatic repair deadline. The platform becomes stronger when users supply failures that maintainers can reproduce and convert into regression tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tier 2 would mean
&lt;/h2&gt;

&lt;p&gt;Promotion would not mean CPython suddenly runs twice as fast on RISC-V. It would mean regressions carry more project-level consequence.&lt;/p&gt;

&lt;p&gt;To sustain Tier 2, RISC-V needs reliable builders, at least two core developers committed to the platform, a repair path that works within 24 hours, and enough confidence that failures can block a Python release without creating chronic instability. Pre-merge CI reduces that risk. Broader user testing supplies the unusual configurations that a small builder fleet misses. More maintainers distribute the response burden.&lt;/p&gt;

&lt;p&gt;There is a useful precedent in WASI. PEP 11 records it as Tier 3 for Python 3.11 and 3.12, then Tier 2 beginning with Python 3.13. The transition happened because support became a maintained process with dependable tooling and people, not because a single patch flipped a switch.&lt;/p&gt;

&lt;p&gt;RISC-V is now at the beginning of the same institutional path. The interpreter runs. The buildbots watch it. Maintainers are named. Native pull-request runners are being explored. Packaging images and wheel tooling exist. Each piece converts individual porting effort into infrastructure that survives the original contributor.&lt;/p&gt;

&lt;p&gt;That is what “official support” is worth: not a badge on an architecture list, but a growing set of promises that can be tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.python.org/2026/08/riscv-now-officially-supported/" rel="noopener noreferrer"&gt;RISC-V is now officially supported by CPython&lt;/a&gt;, the Python core team’s announcement and acknowledgements.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://peps.python.org/pep-0011/" rel="noopener noreferrer"&gt;PEP 11: CPython platform support&lt;/a&gt;, the normative tier definitions and current target matrix.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.riscv.org/reference/isa/v20250508/unpriv/intro.html" rel="noopener noreferrer"&gt;RISC-V unprivileged ISA introduction&lt;/a&gt;, the base-and-extension architecture model.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://riscv-runners.riseproject.dev/docs/getting-started/configure" rel="noopener noreferrer"&gt;RISE RISC-V Runners&lt;/a&gt;, native GitHub Actions runner configuration.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pypa/manylinux" rel="noopener noreferrer"&gt;PyPA manylinux&lt;/a&gt;, portable Linux wheel policies and RISC-V build images.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/" rel="noopener noreferrer"&gt;Python platform compatibility tags&lt;/a&gt;, how installers match wheels to runtimes and systems.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Htmx 4: A Modern Runtime for Server-Driven HTML</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Sat, 29 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/htmx-4-a-modern-runtime-for-server-driven-html-4673</link>
      <guid>https://dev.to/0xgosu/htmx-4-a-modern-runtime-for-server-driven-html-4673</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqami7gdflu42c33frgbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqami7gdflu42c33frgbk.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Htmx has always made a deliberately unfashionable proposition: a server can return HTML, the browser can place that HTML into the document, and a useful application can emerge without duplicating the entire interface as client-side state.&lt;/p&gt;

&lt;p&gt;Version 4 does not abandon that proposition. A button still needs little more than &lt;code&gt;hx-post&lt;/code&gt;, a URL, and a swap rule. What changed is the runtime underneath. The request engine now uses &lt;code&gt;fetch()&lt;/code&gt; instead of &lt;code&gt;XMLHttpRequest&lt;/code&gt;; inherited behavior must be declared; HTTP errors can be rendered as ordinary HTML; morphing and multi-target responses are first-class; and extensions can consume streams without fighting the core.&lt;/p&gt;

&lt;p&gt;That makes htmx 4 an unusual major release. Its public idea remains almost boringly stable while its internal model becomes more native to the modern browser. The result is not a new front-end framework so much as a sharper contract between HTML, HTTP, the DOM, and the server that owns application state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The central idea is still hypermedia
&lt;/h2&gt;

&lt;p&gt;A conventional client-rendered application often asks an API for data, stores that data in JavaScript, renders components, and then reconciles local state with the server. Htmx removes much of that middle layer. The server responds with the representation the user needs: HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;hx-post=&lt;/span&gt;&lt;span class="s"&gt;"/tasks"&lt;/span&gt;
  &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#task-list"&lt;/span&gt;
  &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"beforeend"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Add task
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the button is activated, htmx gathers the request context, calls the server, parses the returned markup, and appends it to &lt;code&gt;#task-list&lt;/code&gt;. The server remains responsible for validation, authorization, persistence, and presentation. The browser remains responsible for interaction, navigation, focus, and the document.&lt;/p&gt;

&lt;p&gt;This is more than a shorter way to write &lt;code&gt;fetch()&lt;/code&gt;. The attributes describe a hypermedia control: which action is available, where it goes, and how its representation should enter the current page. The response can contain links and forms that describe the next valid actions, just like a full document does.&lt;/p&gt;

&lt;p&gt;Htmx 4 keeps that surface while reorganizing the lifecycle below it.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fhtmx-4-hypermedia-runtime%2Ffetch-swap-lifecycle.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fhtmx-4-hypermedia-runtime%2Ffetch-swap-lifecycle.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The browser sends context and receives a representation. Htmx coordinates the trip, but HTML remains the application protocol.   &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;fetch()&lt;/code&gt; changes what the core can become
&lt;/h2&gt;

&lt;p&gt;Htmx previously used &lt;code&gt;XMLHttpRequest&lt;/code&gt; because it worked across an older browser landscape and exposed upload progress events that applications depended on. By 2026, that compatibility choice had become architectural debt. The htmx team rebuilt the request path around the promise-based Fetch API after experimenting with a smaller project called Fixi and with streaming HTML.&lt;/p&gt;

&lt;p&gt;For an ordinary &lt;code&gt;hx-get&lt;/code&gt;, the migration should be invisible. The important difference appears at the edges. Fetch uses standard &lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;, headers, abort signals, modes, credentials, and streaming bodies. Those primitives make the request lifecycle easier to compose with async code and easier for extensions to intercept.&lt;/p&gt;

&lt;p&gt;The new event model reflects that cleaner pipeline. Events now follow a predictable &lt;code&gt;htmx:phase:action[:sub-action]&lt;/code&gt; shape:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Htmx 2&lt;/th&gt;
&lt;th&gt;Htmx 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:beforeRequest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:before:request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:afterRequest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:after:request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:beforeSwap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:before:swap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:afterSwap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:after:swap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:configRequest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:config:request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every request event also receives a consistent context object. An extension no longer needs a loose collection of event-specific details to understand the source element, request configuration, response, and swap. Request completion has a &lt;code&gt;finally&lt;/code&gt; phase, whether the operation succeeds, fails, or is cancelled.&lt;/p&gt;

&lt;p&gt;Htmx also removed wrappers for browser functions that now have dependable native equivalents. &lt;code&gt;htmx.addClass()&lt;/code&gt; becomes &lt;code&gt;element.classList.add()&lt;/code&gt;, &lt;code&gt;htmx.closest()&lt;/code&gt; becomes &lt;code&gt;element.closest()&lt;/code&gt;, and &lt;code&gt;htmx.remove()&lt;/code&gt; becomes &lt;code&gt;element.remove()&lt;/code&gt;. This is healthy subtraction. A small library should not permanently carry convenience APIs once the platform has absorbed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explicit inheritance makes locality visible
&lt;/h2&gt;

&lt;p&gt;The largest migration change is not related to Fetch. It is the decision to stop inheriting most htmx attributes implicitly.&lt;/p&gt;

&lt;p&gt;In htmx 2, a container could put &lt;code&gt;hx-confirm&lt;/code&gt;, &lt;code&gt;hx-target&lt;/code&gt;, or &lt;code&gt;hx-headers&lt;/code&gt; on a parent, and descendant controls would acquire the behavior. That was concise, much like CSS inheritance. It could also make a button’s behavior impossible to understand by reading the button. A security header or destructive confirmation might come from an ancestor several templates away.&lt;/p&gt;

&lt;p&gt;Htmx 4 requires the author to mark that reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;
  &lt;span class="na"&gt;hx-confirm:inherited=&lt;/span&gt;&lt;span class="s"&gt;"Delete this item?"&lt;/span&gt;
  &lt;span class="na"&gt;hx-headers:inherited=&lt;/span&gt;&lt;span class="s"&gt;'{"X-CSRF-Token":"…"}'&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;hx-delete=&lt;/span&gt;&lt;span class="s"&gt;"/items/42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:inherited&lt;/code&gt; suffix is not cosmetic. It tells a reviewer that the attribute is deliberately part of the descendants’ behavior. A child can use &lt;code&gt;:append&lt;/code&gt; when it needs to extend an inherited selector or value instead of replacing it.&lt;/p&gt;

&lt;p&gt;This change moves htmx toward &lt;strong&gt;locality of behavior&lt;/strong&gt; : the closer a declaration is to the element it controls, the less hidden context a reader needs. Shared behavior is still possible, but its scope is advertised at the declaration site.&lt;/p&gt;

&lt;p&gt;It also creates a real upgrade hazard. If a CSRF header previously lived on a layout container, the page may render normally after a version bump while child requests begin failing. The official upgrade checker searches templates and scripts for inheritance, old event names, removed attributes, and obsolete APIs. Treat its report as a starting point, then test the actual request paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error pages are now useful fragments
&lt;/h2&gt;

&lt;p&gt;Htmx 2 did not swap &lt;code&gt;4xx&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt; responses by default. Htmx 4 swaps every HTTP response except &lt;code&gt;204 No Content&lt;/code&gt; and &lt;code&gt;304 Not Modified&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That default is a better fit for server-driven interfaces. Validation failure is still an application state, and the server often has the best information for rendering it. A &lt;code&gt;422&lt;/code&gt; response can contain field errors; a &lt;code&gt;409&lt;/code&gt; can explain a version conflict; a &lt;code&gt;500&lt;/code&gt; can replace a panel with a recovery action. An error status should not force the response body to become invisible.&lt;/p&gt;

&lt;p&gt;Htmx 4 adds &lt;code&gt;hx-status&lt;/code&gt; when different status families need different destinations or swap rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt;
  &lt;span class="na"&gt;hx-post=&lt;/span&gt;&lt;span class="s"&gt;"/account"&lt;/span&gt;
  &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#account-card"&lt;/span&gt;
  &lt;span class="na"&gt;hx-status:422=&lt;/span&gt;&lt;span class="s"&gt;"target:#errors swap:innerHTML"&lt;/span&gt;
  &lt;span class="na"&gt;hx-status:5xx=&lt;/span&gt;&lt;span class="s"&gt;"target:#service-status swap:innerHTML"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- fields --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design consequence belongs on the server: error responses must be valid fragments for the target that will receive them. Status codes continue to carry HTTP meaning, while HTML carries the presentation and the next possible action.&lt;/p&gt;

&lt;h2&gt;
  
  
  One response can update several parts of the page
&lt;/h2&gt;

&lt;p&gt;Server-driven applications frequently need to change more than the clicked element. Adding a message might append to a timeline, update an unread count, and replace a pagination control. Htmx has long supported out-of-band swaps, where specially marked elements in a response replace matching elements elsewhere in the document.&lt;/p&gt;

&lt;p&gt;Htmx 4 adds a clearer tool: &lt;code&gt;&amp;lt;hx-partial&amp;gt;&lt;/code&gt;. Each partial names its target and swap strategy explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new-message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;The build finished.&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;hx-partial&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#messages"&lt;/span&gt; &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"beforeend"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;The build finished.&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/hx-partial&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;hx-partial&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#unread-count"&lt;/span&gt; &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"innerHTML"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;5&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/hx-partial&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main response swaps first. Partials and out-of-band elements follow in document order. That ordering encourages each update to be independently meaningful instead of relying on a hidden side effect from an earlier DOM mutation.&lt;/p&gt;

&lt;p&gt;This is a small but important form of response orchestration. The server can atomically describe the visible consequences of one operation without returning JSON and asking client code to distribute fields among components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Morphing preserves the parts users are touching
&lt;/h2&gt;

&lt;p&gt;Replacing &lt;code&gt;innerHTML&lt;/code&gt; is easy to reason about, but it can destroy local browser state. An input may lose selection, an element may lose focus, a media player may restart, or a custom element may be recreated even though most of its structure did not change.&lt;/p&gt;

&lt;p&gt;Htmx 4 includes &lt;code&gt;innerMorph&lt;/code&gt; and &lt;code&gt;outerMorph&lt;/code&gt; swap modes based on an improved Idiomorph algorithm. Instead of discarding the target subtree, a morphing swap matches old and new nodes and applies the smallest useful set of changes. Stable nodes can keep their identity and therefore retain browser-owned state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"profile"&lt;/span&gt;
  &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/profile/edit"&lt;/span&gt;
  &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"outerMorph"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- current profile --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Morphing is not automatically superior. A simple fragment is often safest to replace wholesale. Morphing earns its complexity when the target contains live controls, custom elements, media, or third-party widgets whose identity matters. Htmx exposes selectors for skipping entire nodes or children during a morph, which helps protect stateful islands.&lt;/p&gt;

&lt;p&gt;The broader pattern is useful: let the server own the desired HTML, but let the browser preserve the physical DOM objects that should survive the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming becomes an extension concern
&lt;/h2&gt;

&lt;p&gt;Moving to Fetch gave htmx a better substrate for streamed responses, but the core does not force one streaming protocol on every application. Htmx 4 ships focused extensions for Server-Sent Events, WebSockets, and multipart responses.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hx-multipart&lt;/code&gt; extension can consume &lt;code&gt;multipart/mixed&lt;/code&gt; or &lt;code&gt;multipart/parallel&lt;/code&gt;. Each part can carry HTML plus its own &lt;code&gt;HX-*&lt;/code&gt; action headers. A server can therefore begin a response with an immediate placeholder, stream later sections as work completes, and target each piece without inventing a client-side message bus.&lt;/p&gt;

&lt;p&gt;SSE remains a good match for ordered one-way server updates. WebSockets remain useful when both browser and server send messages. Multipart works naturally when one HTTP operation produces several representations. The architecture is modular: all three end at the same swap machinery.&lt;/p&gt;

&lt;p&gt;That separation keeps the core small while making the extension boundary more powerful. Htmx 4 extensions register directly and can participate in request, response, and swap phases. They are loaded by including their scripts; &lt;code&gt;hx-ext&lt;/code&gt; is gone. Sites can restrict the allowed extension names through configuration when they want an explicit boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  HCON gives attributes a small structured language
&lt;/h2&gt;

&lt;p&gt;As attributes gained options, htmx needed a notation less noisy than embedded JSON. HCON—htmx Configuration Object Notation—supports space-separated key-value pairs, flag booleans, numbers, quoted strings, and dotted keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"htmx-config"&lt;/span&gt;
  &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"transitions defaultTimeout:5000 sse.reconnect:true"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/report"&lt;/span&gt;
  &lt;span class="na"&gt;hx-config=&lt;/span&gt;&lt;span class="s"&gt;'credentials:"include" cache:"no-cache"'&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Refresh
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON remains valid when a server already produces it. HCON is for handwritten markup: compact enough to scan, structured enough to avoid a growing collection of one-off parsers. The same notation appears in triggers, swap modifiers, request configuration, headers, values, and &lt;code&gt;HX-Location&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;hx-live&lt;/code&gt; handles the client state that remains
&lt;/h2&gt;

&lt;p&gt;Hypermedia does not eliminate every local interaction. Dropdowns open before a request. Character counts change as a user types. Tabs, disclosure widgets, and temporary selections are often browser concerns.&lt;/p&gt;

&lt;p&gt;The new &lt;code&gt;hx-live&lt;/code&gt; extension provides a small DOM-oriented scripting layer for those cases. It includes a query helper, directional selectors, DOM utilities, async helpers, typed access to attributes and data, and reactive bindings such as &lt;code&gt;:text&lt;/code&gt;, &lt;code&gt;:class&lt;/code&gt;, and &lt;code&gt;:hidden&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its important constraint is philosophical: the DOM is the state store. A reactive expression reads nearby element state and updates nearby presentation. Durable application state still belongs on the server and still travels through hypermedia responses.&lt;/p&gt;

&lt;p&gt;That makes &lt;code&gt;hx-live&lt;/code&gt; a pressure valve rather than an invitation to rebuild a second application inside the browser. Use it for ephemeral interaction that would otherwise require repetitive event-listener code. When state must survive navigation, coordinate users, enforce permissions, or participate in transactions, send it to the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  History now favors fresh documents over frozen DOM
&lt;/h2&gt;

&lt;p&gt;Htmx 2 cached history snapshots in &lt;code&gt;localStorage&lt;/code&gt;. Restoring those snapshots could also restore mutations made by unrelated scripts without restoring the JavaScript runtime state that produced them. The page looked alive but contained a fossilized DOM.&lt;/p&gt;

&lt;p&gt;Htmx 4 removes that default cache. Back and forward navigation re-fetches the page and swaps the result into &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; or a designated history element. Correct HTTP caching can make the request cheap while still giving scripts a clean document to initialize.&lt;/p&gt;

&lt;p&gt;Applications that truly need local snapshots can load &lt;code&gt;hx-history-cache&lt;/code&gt;, which uses &lt;code&gt;sessionStorage&lt;/code&gt; and makes the behavior explicit. Again, version 4 chooses an honest boundary: fetching a fresh representation is the default; local reconstruction is an optional capability with a name.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration is a behavioral audit
&lt;/h2&gt;

&lt;p&gt;The safest upgrade is not a blind package replacement. It is a short audit of where behavior crosses markup boundaries.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fhtmx-4-hypermedia-runtime%2Fhtmx4-migration-path.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fhtmx-4-hypermedia-runtime%2Fhtmx4-migration-path.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;Most pages keep their markup. The work is concentrated at implicit inheritance, event listeners, response policy, history, and extensions.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Start with a pinned version and run the checker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx htmx.org@4.0.0 upgrade-check &lt;span class="nt"&gt;--&lt;/span&gt; ./templates

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then review the changes in an order that avoids ambiguous renames:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rename old &lt;code&gt;hx-disable&lt;/code&gt;—which meant “ignore this subtree”—to &lt;code&gt;hx-ignore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rename &lt;code&gt;hx-disabled-elt&lt;/code&gt; to the new &lt;code&gt;hx-disable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;:inherited&lt;/code&gt; wherever a parent must continue affecting descendants, especially headers, confirmation, targets, and includes.&lt;/li&gt;
&lt;li&gt;Update event names and replace removed JavaScript helpers with browser APIs.&lt;/li&gt;
&lt;li&gt;Test &lt;code&gt;4xx&lt;/code&gt; and &lt;code&gt;5xx&lt;/code&gt; responses because they now swap by default.&lt;/li&gt;
&lt;li&gt;Test &lt;code&gt;hx-delete&lt;/code&gt;, which no longer includes enclosing form data unless requested with &lt;code&gt;hx-include&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test back/forward navigation, out-of-band ordering, timeouts, queues, and every installed extension.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are temporary escape hatches. &lt;code&gt;implicitInheritance&lt;/code&gt; can restore the old inheritance default, &lt;code&gt;noSwap&lt;/code&gt; can restore the old error policy, and the &lt;code&gt;htmx-2-compat&lt;/code&gt; extension restores several 2.x behaviors and event names. These are useful for staging, not for ending the migration. A compatibility layer that remains forever leaves the application with two mental models.&lt;/p&gt;

&lt;p&gt;The release itself models similar caution. Htmx 4 is published as version &lt;code&gt;4.0.0&lt;/code&gt;, but the project is keeping the 2.x line under npm’s &lt;code&gt;latest&lt;/code&gt; tag and 4.x under &lt;code&gt;next&lt;/code&gt; until early 2027. That avoids silently upgrading sites that use an unversioned CDN URL. Production HTML should pin an exact version regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Go-native companion: ghtmx
&lt;/h2&gt;

&lt;p&gt;Go teams interested in this server-driven model should also look at &lt;a href="https://github.com/go-monolith/ghtmx" rel="noopener noreferrer"&gt;ghtmx&lt;/a&gt;, created by 0xgosu—the author of this blog. It is a compiled template engine, forked from templ, that treats htmx as a compile-checked language feature rather than a collection of unchecked strings.&lt;/p&gt;

&lt;p&gt;Route-aware &lt;code&gt;hx-*&lt;/code&gt; bindings resolve against actual Go handlers, so renaming a route can break the build at every affected template. Compile-time fragment declarations produce both inline and standalone render entry points, while declared events generate typed emitters for &lt;code&gt;HX-Trigger&lt;/code&gt;. The runtime itself uses only the Go standard library.&lt;/p&gt;

&lt;p&gt;The current library is pre-1.0, and htmx 4.0 support is being added now. Check the &lt;a href="https://github.com/go-monolith/ghtmx" rel="noopener noreferrer"&gt;ghtmx repository&lt;/a&gt; and changelog before adopting it for an htmx 4 application, and pin the version while its language and generated-code surface are still evolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  What htmx 4 is really optimizing
&lt;/h2&gt;

&lt;p&gt;The obvious headline is Fetch. The deeper theme is explicitness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inheritance must say that it crosses a boundary.&lt;/li&gt;
&lt;li&gt;Error bodies are visible representations unless configured otherwise.&lt;/li&gt;
&lt;li&gt;Multi-target updates name their targets and swap rules.&lt;/li&gt;
&lt;li&gt;History either fetches a fresh page or opts into a cache.&lt;/li&gt;
&lt;li&gt;Extensions register directly and use a consistent lifecycle.&lt;/li&gt;
&lt;li&gt;Browser APIs replace library wrappers where the platform is sufficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These choices reduce invisible behavior without requiring the application to move its state into a client framework. That is the balance htmx is trying to hold: capable interactions, server authority, and HTML that still explains what the page can do.&lt;/p&gt;

&lt;p&gt;Htmx 4 will not make every interface simpler. A graphics editor, offline-first workspace, or deeply collaborative local model may need a richer client architecture. But many business applications are primarily navigation, forms, tables, validation, and server-owned workflows. For those systems, returning the finished representation can be simpler than synchronizing two state machines.&lt;/p&gt;

&lt;p&gt;The most impressive part of htmx 4 is therefore what users do not have to relearn. The hypermedia control is still an element. The response is still HTML. The browser is still a browser. The new runtime simply makes that old architecture more at home on the current web platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released" rel="noopener noreferrer"&gt;Htmx 4.0.0 release announcement&lt;/a&gt;, the project’s overview of the release and rollout policy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://four.htmx.org/docs/whats-new-in-htmx-4" rel="noopener noreferrer"&gt;What’s New in htmx 4&lt;/a&gt;, the exhaustive list of breaking changes, new attributes, swaps, events, and extensions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://four.htmx.org/migration-guide-htmx-4/" rel="noopener noreferrer"&gt;Htmx 4 migration guide&lt;/a&gt;, the practical upgrade checklist and compatibility options.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://four.htmx.org/docs/hcon-guide" rel="noopener noreferrer"&gt;HCON guide&lt;/a&gt;, the syntax and locations of htmx’s structured attribute notation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://four.htmx.org/docs/hx-live-guide" rel="noopener noreferrer"&gt;&lt;code&gt;hx-live&lt;/code&gt; programmer’s guide&lt;/a&gt;, the DOM-based scripting and reactive binding model.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>How Tiny Rust Layout Choices Freed 100 TB of DNS Cache Memory</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/how-tiny-rust-layout-choices-freed-100-tb-of-dns-cache-memory-4gmo</link>
      <guid>https://dev.to/0xgosu/how-tiny-rust-layout-choices-freed-100-tb-of-dns-cache-memory-4gmo</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkjjajffhbgnzvmpvvknt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkjjajffhbgnzvmpvvknt.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A byte is usually beneath the resolution of capacity planning. In a cache holding more than 250 billion entries, it is a line item: one extra byte per entry consumes over 250 GB across the fleet.&lt;/p&gt;

&lt;p&gt;That arithmetic changes how software should be designed. A convenient &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; is no longer just an API choice. A repeated domain name is no longer harmless duplication. The padding inside an enum, the size class chosen by an allocator, and the distance between two heap objects all become infrastructure decisions.&lt;/p&gt;

&lt;p&gt;Cloudflare’s Big Pineapple platform—the Rust system behind 1.1.1.1, Gateway DNS, DNS Firewall, and other DNS services—recently applied that way of thinking to its cache. Five related changes cut the benchmarked footprint of an entry from 953 bytes to 420 bytes. Fleet-wide working-set memory fell by roughly 100 TB. The same layout also increased insert throughput by 43% and reduced lookup latency by 19%.&lt;/p&gt;

&lt;p&gt;The interesting lesson is not simply “use less memory.” It is how to make a mature hot-path data structure smaller without turning every read into decompression work. The winning design kept the fields that need interpretation structured, encoded the bulk record data compactly, and arranged the common path so that it could often copy bytes directly into the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache is a second representation of DNS
&lt;/h2&gt;

&lt;p&gt;A recursive resolver receives a question such as “what is the &lt;code&gt;A&lt;/code&gt; record for &lt;code&gt;example.com&lt;/code&gt;?” If the answer is already cached and its time-to-live has not expired, the resolver can answer without repeating the upstream resolution process.&lt;/p&gt;

&lt;p&gt;The key identifies the question. It includes the queried name and record type, plus context such as authentication state and service-specific tags. The value contains metadata and the DNS resource records divided into answer, authority, and additional sections.&lt;/p&gt;

&lt;p&gt;That division follows the DNS message model defined by &lt;a href="https://datatracker.ietf.org/doc/html/rfc1035" rel="noopener noreferrer"&gt;RFC 1035&lt;/a&gt;: a header and question are followed by three lists of resource records. A first implementation naturally mirrors the protocol with Rust structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;CacheEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;additional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// more metadata...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is pleasant code. Each section has its own collection, each record is parsed into a typed enum, and callers can index or inspect fields directly. But this in-memory form is neither the compact DNS wire format nor a database page designed for density. It is a graph of headers, pointers, capacities, enums, and heap allocations.&lt;/p&gt;

&lt;p&gt;The distinction matters: &lt;strong&gt;the representation that is easiest to build is not automatically the representation that should live for millions of lookups&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Big Pineapple fills from empty after startup. Once an instance reaches its configured entry count, it evicts older or less useful records to admit new ones. Cache size varies by location and traffic. EDNS Client Subnet can multiply entries because authoritative servers may return different answers for different client networks. Memory efficiency therefore affects both the number of answers a resolver can retain and the volume of upstream work it must repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the whole path, not just &lt;code&gt;size_of&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Rust’s &lt;code&gt;size_of::&amp;lt;T&amp;gt;()&lt;/code&gt; is useful, but it cannot describe an object graph on its own. A 24-byte vector header may point to a much larger allocation. That allocation may reserve unused capacity. An allocator may round a 40-byte request into a 48-byte size class. Several individually small fields may force alignment padding into a containing struct.&lt;/p&gt;

&lt;p&gt;Cloudflare built a benchmark around generated entries shaped like production traffic: roughly 56% &lt;code&gt;A&lt;/code&gt;, 25% &lt;code&gt;AAAA&lt;/code&gt;, and 19% variable-length records represented by &lt;code&gt;TXT&lt;/code&gt;, with one to four records per entry. A custom allocator wrapper tracked the number and size of allocations. The benchmark also measured insert throughput and lookup latency so a lower byte count could not hide extra CPU work.&lt;/p&gt;

&lt;p&gt;Synthetic measurements were only the first filter. Traffic mix, occupancy, allocator state, and non-cache process data all affect resident memory. Each release was therefore rolled out to production and checked against steady-state working-set measurements.&lt;/p&gt;

&lt;p&gt;That two-level method is worth copying:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use a controlled benchmark to attribute changes to a representation decision.&lt;/li&gt;
&lt;li&gt;Use production memory and latency to verify that the decision survives real distributions and allocator behavior.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  First: stop paying for growth after construction
&lt;/h2&gt;

&lt;p&gt;On a 64-bit system, a &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; conceptually carries a pointer, a length, and a capacity. Capacity is what makes appending efficient: the vector can grow into already reserved space before allocating again.&lt;/p&gt;

&lt;p&gt;A cached DNS answer does not grow. It is assembled once, inserted, read many times, and eventually evicted. Retaining growth machinery throughout that lifetime pays for a capability the value will never use.&lt;/p&gt;

&lt;p&gt;Converting a completed &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; into &lt;code&gt;Box&amp;lt;[T]&amp;gt;&lt;/code&gt; removes the capacity word from the long-lived header. Converting a &lt;code&gt;String&lt;/code&gt; into &lt;code&gt;Box&amp;lt;str&amp;gt;&lt;/code&gt; does the same for text. The &lt;a href="https://doc.rust-lang.org/std/vec/struct.Vec.html" rel="noopener noreferrer"&gt;Rust standard-library documentation&lt;/a&gt; explicitly recommends a boxed slice when excess capacity should be discarded and the long-lived structure should be smaller.&lt;/p&gt;

&lt;p&gt;Each cache entry had eight vector or string fields. Removing one 8-byte capacity word from each saved 64 bytes in the entry header, before accounting for abandoned spare slots in the backing allocations. Multiplied across the cache, this first change was already worth more than 15 TB.&lt;/p&gt;

&lt;p&gt;This is a general ownership rule: use growable containers during assembly, then freeze them at the boundary into the smallest type that expresses the stored value’s real behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second: represent boundaries with offsets, not allocations
&lt;/h2&gt;

&lt;p&gt;Answer, authority, and additional records are logically separate, but they do not need three independent heap objects. They can occupy one contiguous sequence with two offsets marking where the later sections begin.&lt;/p&gt;

&lt;p&gt;Because DNS sections contain small record counts, each boundary fits in a &lt;code&gt;u16&lt;/code&gt;. Two boxed slices would each require a pointer and a length—16 bytes apiece on a 64-bit target. Replacing those two extra slices with two 2-byte offsets saves 28 bytes per entry and removes two allocation relationships.&lt;/p&gt;

&lt;p&gt;The same reasoning applies to flags. Several booleans can be packed into one bit field. The direct savings may look tiny, yet layout changes are not perfectly additive. Rust must respect field alignment, and a structure’s total size is rounded to its alignment. Removing a two-byte field or collecting booleans can eliminate neighboring padding as well. The &lt;a href="https://doc.rust-lang.org/reference/type-layout.html" rel="noopener noreferrer"&gt;Rust Reference&lt;/a&gt; explains why the size of a value includes this alignment padding.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdns-cache-memory-layout%2Fcache-entry-layout-evolution.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdns-cache-memory-layout%2Fcache-entry-layout-evolution.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;Most savings came from changing relationships: one allocation instead of several, one shared name instead of copies, and offsets instead of pointer-length pairs.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Contiguity also improves locality. A lookup that touches adjacent bytes can make better use of each CPU cache line than one that follows pointers across unrelated heap regions. At this scale, memory footprint and latency are often the same problem viewed from different levels of the hierarchy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third: infer the owner on the common path
&lt;/h2&gt;

&lt;p&gt;Every DNS resource record has an owner name. In an ordinary answer, that owner is often exactly the name in the cache key. A query for &lt;code&gt;example.com A&lt;/code&gt; might return two &lt;code&gt;A&lt;/code&gt; records, both owned by &lt;code&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Storing the full owner inside both records duplicates information already held by the key. Big Pineapple changed the record to store an optional owner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Record&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Rtype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RecordData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;None&lt;/code&gt; means “use the queried name from the cache key.” A different name is stored only when the response requires one, as in a &lt;code&gt;CNAME&lt;/code&gt; chain where later records belong to the canonical target.&lt;/p&gt;

&lt;p&gt;This deliberately makes an individual record less self-contained. That is a good trade because every cache lookup already has the key. The complete context exists at the point where the record is read, so copying it into every stored child object buys convenience rather than capability.&lt;/p&gt;

&lt;p&gt;DNS already makes a similar trade on the network. RFC 1035 name compression allows later names to refer to an earlier suffix with a two-octet pointer. Big Pineapple did not keep those compression pointers in its parsed cache representation—the lookup cost would be awkward—but it borrowed the deeper idea: repeated context can be referenced or inferred instead of expanded everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fourth: stop sizing common records for the rare giant
&lt;/h2&gt;

&lt;p&gt;A typed enum looks ideal for DNS record data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;RecordData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;A&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ipv4Addr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Aaaa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ipv6Addr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Txt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Txt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Naptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Naptr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;Svcb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Svcb&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// many more variants&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An enum value must have enough space for any of its variants, plus the discriminant and padding. In this case, &lt;code&gt;NAPTR&lt;/code&gt; was the largest variant at 136 bytes, making the full enum 144 bytes. Yet &lt;code&gt;A&lt;/code&gt; needs four bytes and &lt;code&gt;AAAA&lt;/code&gt; needs sixteen, and together they represented more than 80% of the benchmark’s traffic.&lt;/p&gt;

&lt;p&gt;The common records were therefore carrying well over a hundred bytes of unused inline space so the same slot could occasionally hold a rare large record.&lt;/p&gt;

&lt;p&gt;The first repair boxed large variants. Small, common variants remained inline; large variants became pointers to right-sized heap allocations. This reduced the enum to 24 bytes and saved about 120 bytes for each &lt;code&gt;A&lt;/code&gt; or &lt;code&gt;AAAA&lt;/code&gt; record.&lt;/p&gt;

&lt;p&gt;Boxing is a useful lever, but it introduces a second bill. Each large value needs an allocation. Allocators such as &lt;a href="https://jemalloc.net/" rel="noopener noreferrer"&gt;jemalloc&lt;/a&gt; group requests into size classes, so a requested size may be rounded upward. Pointer chasing also scatters related data and can cause extra CPU cache misses.&lt;/p&gt;

&lt;p&gt;The intermediate enum proved the distribution-aware idea—optimize the container for common variants instead of the maximum—but it was not the final layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifth: keep structure where it helps, wire bytes where they win
&lt;/h2&gt;

&lt;p&gt;One extreme would cache an entire ready-to-send DNS packet. That is compact, but responses are not identical for every client. The transaction ID changes. DNSSEC records are included only when the client sets the DNSSEC OK flag. Name compression depends on the final message layout. A full packet would need duplicate variants or parsing and filtering on every hit.&lt;/p&gt;

&lt;p&gt;The opposite extreme is the fully parsed object graph. It makes manipulation easy but spends memory and serialization work on fields that are usually copied back to the wire.&lt;/p&gt;

&lt;p&gt;The final design sits between them. Metadata remains structured. Record payloads are encoded into one &lt;code&gt;Box&amp;lt;[u8]&amp;gt;&lt;/code&gt;; each record is stored as a two-byte length followed by its wire-format bytes.&lt;/p&gt;

&lt;p&gt;Most records—including &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, &lt;code&gt;TXT&lt;/code&gt;, and DNSSEC types—can then be copied directly into the outgoing response. Records containing domain names, such as &lt;code&gt;CNAME&lt;/code&gt;, &lt;code&gt;NS&lt;/code&gt;, &lt;code&gt;MX&lt;/code&gt;, and &lt;code&gt;SOA&lt;/code&gt;, are parsed when needed so the resolver can apply name compression in the new message.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdns-cache-memory-layout%2Fhybrid-cache-lookup-path.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fdns-cache-memory-layout%2Fhybrid-cache-lookup-path.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The hybrid layout does not compress everything. It preserves just enough structure to specialize the response while letting the dominant record types bypass field-by-field serialization.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Random indexing becomes less convenient because the length-prefixed buffer must be scanned. Big Pineapple sometimes rotates address records for round-robin behavior, but entries contain few records, so sequential iteration is cheap. In exchange, all payloads occupy one allocation and arrive together in cache lines.&lt;/p&gt;

&lt;p&gt;Insertion uses a reusable scratch buffer. Repeated writes grow it to a practical high-water mark, so building the next entry usually needs no temporary reallocation. Once serialization is complete, the exact byte count is copied into a boxed slice. That creates one right-sized long-lived allocation and avoids hoping that shrinking a vector will return its unused tail to the allocator. This last change alone raised insert throughput by 13% in the benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller became faster because less work moved through the machine
&lt;/h2&gt;

&lt;p&gt;The completed set of changes reduced the benchmarked net footprint per entry by 56%, from 953 to 420 bytes. Allocations per entry fell from about 1.1 KB to 461 bytes, a 58% reduction.&lt;/p&gt;

&lt;p&gt;Performance moved in the same direction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache insertion&lt;/td&gt;
&lt;td&gt;625,000 entries/s&lt;/td&gt;
&lt;td&gt;893,000 entries/s&lt;/td&gt;
&lt;td&gt;+43%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache lookup latency&lt;/td&gt;
&lt;td&gt;828 ns&lt;/td&gt;
&lt;td&gt;670 ns&lt;/td&gt;
&lt;td&gt;-19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-entry footprint&lt;/td&gt;
&lt;td&gt;953 bytes&lt;/td&gt;
&lt;td&gt;420 bytes&lt;/td&gt;
&lt;td&gt;-56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-entry allocations&lt;/td&gt;
&lt;td&gt;1.1 KB&lt;/td&gt;
&lt;td&gt;461 bytes&lt;/td&gt;
&lt;td&gt;-58%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not mysterious once the hierarchy is visible. Fewer allocations mean less allocator bookkeeping. Smaller values move fewer bytes during insertion. Contiguous records need fewer pointer dereferences and use CPU cache lines more fully. Direct copying avoids reconstructing common records field by field.&lt;/p&gt;

&lt;p&gt;During the production rollout, instance memory fell in steps as new releases arrived and caches refilled. At the 99th percentile, steady-state resident memory dropped from 9.3 GB to 5.3 GB; at the 90th percentile it fell from 6.5 GB to 3.8 GB. Across the fleet, the working-set reduction settled near 100 TB.&lt;/p&gt;

&lt;p&gt;The freed memory is not merely an accounting win. Cloudflare plans to spend it on more cache entries. A larger cache raises hit rate, which reduces upstream queries and can improve response time and resilience. Efficient representation creates a compounding return: the same hardware retains more answers and performs less external work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical playbook for high-scale data structures
&lt;/h2&gt;

&lt;p&gt;The techniques here are specific to DNS, but the design process applies to search indexes, telemetry buffers, object stores, compilers, databases, and any service with millions of long-lived objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model the real distribution
&lt;/h3&gt;

&lt;p&gt;Do not optimize every enum variant equally. Measure which cases dominate count, bytes, and access frequency. A rare large value should not dictate the inline size of billions of common small values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate construction from storage
&lt;/h3&gt;

&lt;p&gt;Builders benefit from mutation and spare capacity. Stored objects benefit from exact sizing and immutability. Make that phase transition explicit with frozen strings, boxed slices, arenas, pages, or another compact representation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Charge pointers the full price
&lt;/h3&gt;

&lt;p&gt;A pointer costs more than its width. It implies an allocation, allocator metadata, possible size-class slack, another object lifetime, and a potential cache miss. Replacing two pointers with a four-byte offset can save far more than twelve bytes in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat duplicated context as a schema question
&lt;/h3&gt;

&lt;p&gt;If every child repeats a value available from its parent or key, ask whether the child truly needs to stand alone. Optional overrides are often enough. The tradeoff must be explicit: inference saves space but increases coupling to the surrounding context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose an internal wire format deliberately
&lt;/h3&gt;

&lt;p&gt;Parsed structures and serialized bytes are endpoints, not commandments. A hybrid can keep searchable metadata typed while storing opaque payloads compactly. The right boundary is the one that minimizes total work across insertion, residency, and lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate improvements at several layers
&lt;/h3&gt;

&lt;p&gt;Track type sizes, heap allocations, throughput, tail latency, resident memory, and downstream behavior such as cache hit rate. An optimization that wins one microbenchmark can lose after allocator rounding or production traffic shifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real optimization was removing capabilities
&lt;/h2&gt;

&lt;p&gt;Every major saving came from taking something away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;growable capacity after the value became immutable;&lt;/li&gt;
&lt;li&gt;independent allocations for sections that can share storage;&lt;/li&gt;
&lt;li&gt;owner names already present in the key;&lt;/li&gt;
&lt;li&gt;inline room for rare enum variants;&lt;/li&gt;
&lt;li&gt;parsed fields that the hot path only serializes again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the durable lesson. Compact systems are not created by clever packing alone. They emerge when the representation offers exactly the capabilities its lifetime requires—and no more.&lt;/p&gt;

&lt;p&gt;At ordinary scale, these choices look like implementation details. At 250 billion entries, they become 100 terabytes, 268,000 additional inserts per second, and 158 fewer nanoseconds on a cache hit. Scale does not invent new costs. It reveals the ones that were already hiding in every object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/dns-cache-memory-optimization-1111/" rel="noopener noreferrer"&gt;Cloudflare: How we saved 100 terabytes of memory by optimizing 1.1.1.1’s DNS cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/big-pineapple-intro/" rel="noopener noreferrer"&gt;Cloudflare: How Rust and Wasm power 1.1.1.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc1035" rel="noopener noreferrer"&gt;RFC 1035: Domain names—implementation and specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/std/vec/struct.Vec.html" rel="noopener noreferrer"&gt;Rust standard library: &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.rust-lang.org/reference/type-layout.html" rel="noopener noreferrer"&gt;The Rust Reference: type layout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jemalloc.net/" rel="noopener noreferrer"&gt;jemalloc project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>When Unicode Version Drift Becomes a Security Bug</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Thu, 27 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/when-unicode-version-drift-becomes-a-security-bug-49pl</link>
      <guid>https://dev.to/0xgosu/when-unicode-version-drift-becomes-a-security-bug-49pl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffw3qf6k7hf14slf4y26o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffw3qf6k7hf14slf4y26o.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;str.lower()&lt;/code&gt; looks too ordinary to be dangerous. It takes text, returns lowercase text, and appears in codebases everywhere. Yet inside a standards implementation, even a familiar string method can cross a security boundary.&lt;/p&gt;

&lt;p&gt;That happened in Python’s implementation of Stringprep, the text-preparation framework used by the original internationalized domain-name standard. The protocol was built from Unicode 3.2 data published in 2002. Python kept the old database needed by the protocol, but one fallback in the case-folding path called the ordinary &lt;code&gt;str.lower()&lt;/code&gt; method. That method follows the Unicode database bundled with the running interpreter.&lt;/p&gt;

&lt;p&gt;The result was a split-brain algorithm: most of the implementation lived in 2003, while one operation silently moved forward whenever Python adopted a newer Unicode release.&lt;/p&gt;

&lt;p&gt;This is not the usual Unicode warning about characters that merely look alike. The deeper problem is &lt;strong&gt;version drift inside a supposedly deterministic protocol&lt;/strong&gt;. If a stored identifier, certificate rule, allowlist, cache key, or network destination is prepared differently on two machines, those machines may disagree about what resource a user meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain names need a bridge between Unicode and ASCII
&lt;/h2&gt;

&lt;p&gt;The Domain Name System was designed around a narrow set of ASCII characters. Human language is not. Internationalized Domain Names in Applications, or IDNA, bridge that gap by transforming a Unicode label into an ASCII form that starts with &lt;code&gt;xn--&lt;/code&gt;. The reversible encoding at the end is Punycode.&lt;/p&gt;

&lt;p&gt;Punycode alone is not enough. Before encoding, an application must decide which inputs should compare as equivalent, which characters should disappear, which are forbidden, and how bidirectional text should behave. IDNA 2003 delegated that preparation to &lt;strong&gt;Nameprep&lt;/strong&gt; , a profile of the more general &lt;strong&gt;Stringprep&lt;/strong&gt; framework in &lt;a href="https://datatracker.ietf.org/doc/html/rfc3454" rel="noopener noreferrer"&gt;RFC 3454&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stringprep describes a pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Map selected characters, including case mappings.&lt;/li&gt;
&lt;li&gt;Normalize the result with Unicode NFKC when the profile requires it.&lt;/li&gt;
&lt;li&gt;Reject prohibited output.&lt;/li&gt;
&lt;li&gt;Apply bidirectional-text rules.&lt;/li&gt;
&lt;li&gt;Pass the prepared label to the protocol-specific encoder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fpython-unicode-version-drift%2Fstringprep-version-boundary.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fpython-unicode-version-drift%2Fstringprep-version-boundary.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The protocol is a versioned pipeline. One moving stage is enough to make the output unstable.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Every stage is part of the meaning of the resulting identifier. Changing one is not a harmless implementation improvement. It changes the protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard deliberately froze Unicode at version 3.2
&lt;/h2&gt;

&lt;p&gt;Unicode evolves. New scripts are added, previously unassigned code points acquire characters, and properties can be refined. Casing is data-driven too. A character that had no lowercase partner in one release can gain one later.&lt;/p&gt;

&lt;p&gt;That evolution is healthy for general text processing. It is dangerous when a wire protocol expects a fixed answer.&lt;/p&gt;

&lt;p&gt;RFC 3454 therefore did not say “use whatever Unicode knows today.” Its tables were generated from Unicode 3.2. Appendix B records the mappings required for case folding, while other appendices record unassigned and prohibited code points. Two independent implementations using those tables should produce the same prepared string even if one runs decades after the other.&lt;/p&gt;

&lt;p&gt;Python supports this requirement in an unusually explicit way. The standard library exposes the interpreter’s current Unicode database through &lt;code&gt;unicodedata&lt;/code&gt;, but it also ships &lt;code&gt;unicodedata.ucd_3_2_0&lt;/code&gt;. The legacy database exists specifically for protocols such as Stringprep and IDNA 2003.&lt;/p&gt;

&lt;p&gt;You can see the two clocks in one interpreter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unidata_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# for example, "15.1.0"
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ucd_3_2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unidata_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# "3.2.0"
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both databases are correct. They answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The current database answers, “What does this character mean under the Unicode version supported by this Python runtime?”&lt;/li&gt;
&lt;li&gt;The legacy database answers, “What did Unicode 3.2 say, as required by this old protocol?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The vulnerability appeared because one code path asked the first question when the protocol required the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  A convenient fallback escaped the frozen database
&lt;/h2&gt;

&lt;p&gt;Stringprep’s B.3 table contains case-folding exceptions. Python represented those exceptions in a lookup and used a fallback for every code point not present in the table. In simplified form, the code looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map_table_b3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b3_exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lookup was frozen. The fallback was not.&lt;/p&gt;

&lt;p&gt;At the time the table was created, calling lowercase for an ordinary character looked equivalent to storing every identity or simple lowercase mapping explicitly. Years later, &lt;code&gt;character.lower()&lt;/code&gt; had learned about characters and casing relationships that Unicode 3.2 did not contain. The shortcut stopped being equivalent to the table it was meant to implement.&lt;/p&gt;

&lt;p&gt;Cherokee makes the failure concrete. Unicode originally encoded Cherokee letters in an uppercase-like form. Lowercase Cherokee letters were added much later. Under a modern Unicode database, the Cherokee letter &lt;code&gt;Ꭰ&lt;/code&gt; (U+13A0) lowercases to &lt;code&gt;ꭰ&lt;/code&gt; (U+AB70). Under Unicode 3.2, U+AB70 was not the lowercase counterpart used by Stringprep; it was outside the frozen repertoire.&lt;/p&gt;

&lt;p&gt;On an affected Python runtime, this happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ᎠᎠ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;# ꭰꭰ
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idna&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# b'xn--kz9aa' # affected behavior
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RFC-bound result preserves the Unicode 3.2 interpretation and encodes the original pair as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xn--58da

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is not cosmetic. Those ASCII labels name different DNS locations. A runtime upgrade can therefore change which hostname an application contacts even though the source Unicode string is unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this crosses a security boundary
&lt;/h2&gt;

&lt;p&gt;Not every standards mismatch is exploitable. The risk appears when software makes a security decision on one representation and performs an action on another.&lt;/p&gt;

&lt;p&gt;Consider a service that accepts a Unicode hostname and applies an allowlist before making an outbound request. The validator runs in one process or container; the network client runs in another. If their Python versions carry different Unicode data, they can derive different ASCII labels from the same input.&lt;/p&gt;

&lt;p&gt;The same pattern can affect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL allowlists and blocklists;&lt;/li&gt;
&lt;li&gt;certificate hostname checks;&lt;/li&gt;
&lt;li&gt;proxy routing and egress policy;&lt;/li&gt;
&lt;li&gt;cookie and origin boundaries;&lt;/li&gt;
&lt;li&gt;caches indexed by normalized hostnames;&lt;/li&gt;
&lt;li&gt;database uniqueness constraints;&lt;/li&gt;
&lt;li&gt;signed records that are normalized again during verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dangerous shape is a &lt;strong&gt;check/use split&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unicode input
    ├── validator with mapping version A → identifier A → allowed
    └── connector with mapping version B → identifier B → contacted

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when every component is locally consistent, the system is not. The bug lives in the relationship between them.&lt;/p&gt;

&lt;p&gt;Stored data adds a second failure mode. Suppose an application canonicalizes a username or hostname, stores only the canonical value, and later recomputes that value after an upgrade. A new mapping can create collisions, orphan old records, or cause lookups to reach a different row. Unicode upgrades then behave like schema migrations, except the schema change is hidden inside a library call.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is different from homoglyph spoofing
&lt;/h2&gt;

&lt;p&gt;Unicode security discussions often begin with confusables: a Cyrillic character that resembles a Latin one, mixed scripts in an identifier, or invisible formatting characters. Those are real concerns, and &lt;a href="https://www.unicode.org/reports/tr39/" rel="noopener noreferrer"&gt;Unicode Technical Standard #39&lt;/a&gt; provides restriction levels and confusable-skeleton mechanisms to help detect them.&lt;/p&gt;

&lt;p&gt;Version drift is a different class of failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confusable text&lt;/strong&gt; gives different code points similar visual appearances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalization differences&lt;/strong&gt; give canonically or compatibly related sequences inconsistent binary forms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case-folding differences&lt;/strong&gt; change which strings compare without case distinctions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift&lt;/strong&gt; makes any of those transformations depend on the library or runtime version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solving only the visual problem does not fix the protocol problem. A string can contain no deceptive glyphs and still map differently because two components use different Unicode data. Likewise, casefolding is not a universal security sanitizer. Python’s &lt;a href="https://docs.python.org/3/library/stdtypes.html#str.casefold" rel="noopener noreferrer"&gt;&lt;code&gt;str.casefold()&lt;/code&gt;&lt;/a&gt; is designed for caseless matching under the runtime’s current Unicode version. It is more aggressive than &lt;code&gt;lower()&lt;/code&gt;—for example, it maps German &lt;code&gt;ß&lt;/code&gt; to &lt;code&gt;ss&lt;/code&gt;—but it is still the wrong tool when a protocol mandates an older, fixed mapping table.&lt;/p&gt;

&lt;p&gt;The correct operation depends on the contract, not on which string method sounds strongest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repair makes the protocol data complete again
&lt;/h2&gt;

&lt;p&gt;The CPython fix did not freeze &lt;code&gt;str.lower()&lt;/code&gt; globally. General Python code should continue receiving current Unicode behavior. Instead, the repair regenerated Stringprep’s data so that every code point whose modern lowercase behavior differs from Unicode 3.2 gets an explicit protocol-specific mapping.&lt;/p&gt;

&lt;p&gt;Conceptually, the corrected function becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map_table_b3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;protocol_exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# safe only after all version differences are covered
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is how &lt;code&gt;protocol_exceptions&lt;/code&gt; is built. CPython’s generation tooling walks the Unicode code-point space, compares the modern behavior with the Unicode 3.2 database and the RFC tables, and records the differences. Regression tests then pin known examples including Cherokee, Georgian, Cyrillic, and Roman numeral characters.&lt;/p&gt;

&lt;p&gt;The merged &lt;a href="https://github.com/python/cpython/pull/155293" rel="noopener noreferrer"&gt;CPython change&lt;/a&gt; also tightened property handling for code points outside RFC 3454. The accompanying security note states the intended invariant plainly: &lt;code&gt;stringprep&lt;/code&gt; and the built-in &lt;code&gt;encodings.idna&lt;/code&gt; codec must not consider Unicode attributes beyond those defined by the RFC.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fpython-unicode-version-drift%2Funicode-drift-and-repair.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Fpython-unicode-version-drift%2Funicode-drift-and-repair.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The fix restores a single clock: protocol behavior comes from the protocol’s data, not the runtime’s release date.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;This repair pattern is broadly useful. When an old specification and a living database disagree, isolate the compatibility behavior in generated, reviewable data. Do not weaken the modern API for every caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  IDNA 2003 and IDNA 2008 are different protocols
&lt;/h2&gt;

&lt;p&gt;It is tempting to respond by replacing every call to Python’s built-in &lt;code&gt;idna&lt;/code&gt; codec with a newer method. For new applications, moving away from IDNA 2003 is usually the right direction—but migration must be explicit.&lt;/p&gt;

&lt;p&gt;Python’s built-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idna&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;implements IDNA 2003. The third-party &lt;a href="https://pypi.org/project/idna/" rel="noopener noreferrer"&gt;&lt;code&gt;idna&lt;/code&gt; package&lt;/a&gt; implements the newer IDNA 2008 family and Unicode Technical Standard #46 processing. The standards differ in character validity, mappings, and compatibility behavior. Swapping implementations can change accepted input and output, so it deserves tests and a data-migration plan rather than a blind dependency edit.&lt;/p&gt;

&lt;p&gt;IDNA 2008 largely moved away from Stringprep’s model of embedding a snapshot of Unicode mappings. Its rules are designed to work with evolving Unicode properties while preserving stronger stability constraints. That makes the newer family a better basis for contemporary internationalized domains, but it does not erase deployed IDNA 2003 data or interoperability requirements.&lt;/p&gt;

&lt;p&gt;Applications sometimes need both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compatibility with stored or remote IDNA 2003 identifiers;&lt;/li&gt;
&lt;li&gt;IDNA 2008 or UTS #46 for new user input;&lt;/li&gt;
&lt;li&gt;an explicit boundary that says which version owns each value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The version must travel with the data or be unambiguous from context. A bare string is not enough when two standards can interpret it differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer design for security-sensitive text
&lt;/h2&gt;

&lt;p&gt;This incident suggests a practical engineering checklist.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Name the exact transformation
&lt;/h3&gt;

&lt;p&gt;Avoid helpers called &lt;code&gt;normalize_name()&lt;/code&gt; or &lt;code&gt;clean_host()&lt;/code&gt;. Use names that expose the contract, such as &lt;code&gt;to_idna2008_ascii()&lt;/code&gt; or &lt;code&gt;nfkc_casefold_identifier_v17()&lt;/code&gt;. Precise names make accidental substitutions easier to spot in review.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Canonicalize once, then pass the canonical value
&lt;/h3&gt;

&lt;p&gt;Do not let an allowlist, proxy, HTTP client, logger, and database each normalize the same raw input. Convert at one trusted boundary, validate the canonical result, and pass that exact result to the operation. This collapses the check/use split.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Record algorithm and data versions
&lt;/h3&gt;

&lt;p&gt;If canonical forms are stored, include the transformation version in the schema or metadata. Treat upgrades as migrations. Recompute in a staging job, look for new collisions and changed keys, and decide how old records will be addressed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test non-ASCII edge cases across supported runtimes
&lt;/h3&gt;

&lt;p&gt;ASCII-only fixtures will never expose Unicode drift. Build a corpus from specification tables, historical regressions, newly assigned code points, multi-code-point folds, normalization boundaries, and bidirectional cases. Run it against every supported runtime and architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Compare components, not just functions
&lt;/h3&gt;

&lt;p&gt;An implementation can pass its own unit tests while disagreeing with another service. Add end-to-end tests that send the canonical identifier from policy enforcement to the network or storage layer. Verify the exact bytes at both ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Prefer generated tables with provenance
&lt;/h3&gt;

&lt;p&gt;Hand-maintained exception lists decay. Generate them from pinned upstream data, store checksums or version identifiers, and make diffs reviewable. A table may be large, but it is often safer than a clever fallback whose behavior is inherited from the host runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Separate display text from identity keys
&lt;/h3&gt;

&lt;p&gt;Users should see the text they entered, with appropriate spoofing warnings. Internal equality and routing should use a clearly defined canonical key. A confusable skeleton is a detection aid, not display text and not a general-purpose normalized identifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The larger lesson: standards code has a time dimension
&lt;/h2&gt;

&lt;p&gt;Developers usually review a function by asking what inputs it accepts and what outputs it returns. Versioned text processing adds a third dimension: &lt;strong&gt;when&lt;/strong&gt; the function’s data was defined.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;str.lower()&lt;/code&gt; was not inherently unsafe. It was unsafe because its contract was “current Unicode lowercase,” while its caller’s contract was “the case mapping frozen into RFC 3454.” Both behaved as designed. The composition did not.&lt;/p&gt;

&lt;p&gt;That pattern reaches far beyond domain names. Cryptographic suites, MIME registries, time-zone data, certificate roots, locale rules, parsers, and database collations all mix living software with versioned external knowledge. A convenient platform API can silently import today’s answer into a protocol that requires yesterday’s.&lt;/p&gt;

&lt;p&gt;The safest question in standards code is therefore not only “Is this function correct?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Correct according to which data version, and will every component use the same one?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once text influences identity, authorization, routing, or signatures, Unicode data is no longer a presentation detail. It is part of the security protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sethmlarson.dev/when-str-lower-is-a-security-vulnerability" rel="noopener noreferrer"&gt;When &lt;code&gt;str.lower()&lt;/code&gt; is a security vulnerability in Python&lt;/a&gt;, Seth Larson’s account of the discovery and repair.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc3454" rel="noopener noreferrer"&gt;RFC 3454: Preparation of Internationalized Strings&lt;/a&gt;, the Stringprep framework and its Unicode 3.2 tables.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/python/cpython/pull/155293" rel="noopener noreferrer"&gt;CPython pull request #155293&lt;/a&gt;, the generated compatibility data, regression tests, and security note.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/unicodedata.html" rel="noopener noreferrer"&gt;Python &lt;code&gt;unicodedata&lt;/code&gt; documentation&lt;/a&gt;, including the legacy &lt;code&gt;ucd_3_2_0&lt;/code&gt; database.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.unicode.org/reports/tr39/" rel="noopener noreferrer"&gt;Unicode Technical Standard #39&lt;/a&gt;, identifier restrictions and confusable detection.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc5890" rel="noopener noreferrer"&gt;RFC 5890: Internationalized Domain Names for Applications&lt;/a&gt;, the terminology and architecture of IDNA 2008.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Teaching a Language Model to Paint with Editable Code</title>
      <dc:creator>Vincent Tran</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/0xgosu/teaching-a-language-model-to-paint-with-editable-code-2ig5</link>
      <guid>https://dev.to/0xgosu/teaching-a-language-model-to-paint-with-editable-code-2ig5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki6oc8nyjhx9979270ih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fki6oc8nyjhx9979270ih.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most image generators give us one narrow way to participate: describe an image, wait for pixels, and ask the model to try again. The result may be beautiful, but it is also flattened. We cannot select the loop that drew one petal, change the color rule behind a wash, or make the composition denser by editing the artifact itself.&lt;/p&gt;

&lt;p&gt;Surya Narreddi and collaborators explored a different medium. They trained a language model to write complete JavaScript sketches using &lt;strong&gt;p5.brush&lt;/strong&gt; , rendered those programs into watercolor-like images, and improved the model with reinforcement learning. The deliverable is not only a PNG. It is a program that remains available for inspection and revision.&lt;/p&gt;

&lt;p&gt;That apparently small change—from generating pixels to generating the procedure behind the pixels—reshapes the whole system. Rendering becomes a tool call. Compilation becomes a hard correctness check. Aesthetic taste must become a reward. Prompt design becomes part of the action space. And because there is no objective answer to “is this a good watercolor?”, the hardest engineering work moves into the evaluator.&lt;/p&gt;

&lt;p&gt;The project is therefore about more than flowers. It is a compact case study in how to train models for subjective work without reducing taste to a vague score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code is a more participatory image format
&lt;/h2&gt;

&lt;p&gt;A raster image stores a finished grid of colors. A creative-coding sketch stores decisions: brush selection, pigment, stroke order, geometry, randomness, density, and layering. Rendering turns those decisions into pixels, but the decisions remain editable.&lt;/p&gt;

&lt;p&gt;That creates several useful properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local changes are possible.&lt;/strong&gt; A person can adjust the stem, petal count, palette, or background without asking the model to regenerate everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior is inspectable.&lt;/strong&gt; The source reveals whether the image came from careful structure, accidental complexity, or an invented API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results can be reproduced.&lt;/strong&gt; With fixed code, dependencies, canvas dimensions, and random seeds, the image can be rendered again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The medium can be extended.&lt;/strong&gt; The same sketch can become an animation, print, parameterized series, or interactive tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure is legible.&lt;/strong&gt; A blank canvas can be traced to syntax, unsupported methods, bad coordinates, or a poor visual idea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is equally important. Code does not magically make image creation easier. It adds a runtime, an API, a sandbox, and a much longer path from intent to picture. Direct image models are faster and have absorbed far more visual knowledge. The value here is agency over the artifact, not raw convenience.&lt;/p&gt;

&lt;p&gt;The chosen drawing layer, &lt;a href="https://github.com/acamposuribe/p5.brush" rel="noopener noreferrer"&gt;p5.brush&lt;/a&gt;, adds natural-media tools to p5.js: pencils, charcoal, markers, watercolor fills, hatching, and vector fields that bend strokes. Instead of asking a language model to emit millions of pixels, the system asks it to compose a relatively small vocabulary of visual operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The training system closes a tool-use loop
&lt;/h2&gt;

&lt;p&gt;Each training example begins with a visual request such as a peach hibiscus painted in watercolor. The language model produces a complete p5.brush program. Puppeteer runs the sketch in a browser environment and captures the canvas as a PNG. A separate model compares the result with curated reference images, and that preference is converted into the reward used to update the code-generating model.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Ftraining-ai-to-paint-with-code%2Fcode-painting-training-loop.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Ftraining-ai-to-paint-with-code%2Fcode-painting-training-loop.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;The model acts in code, but the learning signal comes from the rendered image.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The loop connects two different spaces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token space&lt;/strong&gt; , where the policy chooses JavaScript tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution space&lt;/strong&gt; , where the sketch either runs or fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image space&lt;/strong&gt; , where pigment, composition, depth, and resemblance become visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preference space&lt;/strong&gt; , where a judge decides which image better represents the target taste.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation is powerful. Syntax and API use can be checked exactly. The final visual result can be judged holistically. A program that compiles but paints a dull icon is valid code and poor art; a beautiful-looking snippet that calls imaginary functions never reaches the canvas. The reward has to recognize both cases.&lt;/p&gt;

&lt;p&gt;It also creates an obvious security boundary. Model-written JavaScript is untrusted input. A production renderer needs process isolation, time and memory limits, restricted networking and filesystem access, deterministic dependencies, and an actual browser sandbox. Puppeteer’s own &lt;a href="https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md" rel="noopener noreferrer"&gt;troubleshooting guidance&lt;/a&gt; strongly discourages running Chromium without its sandbox. Creative output does not make generated code safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the first reward function failed
&lt;/h2&gt;

&lt;p&gt;The first training rubric looked comprehensive. It contained nine signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the sketch compiled;&lt;/li&gt;
&lt;li&gt;whether it actually used p5.brush rather than plain p5.js;&lt;/li&gt;
&lt;li&gt;a code-length ramp aimed around 3,000 tokens;&lt;/li&gt;
&lt;li&gt;HPSv3, an image preference model;&lt;/li&gt;
&lt;li&gt;prompt adherence from a council of judge models;&lt;/li&gt;
&lt;li&gt;recognizability;&lt;/li&gt;
&lt;li&gt;aesthetics;&lt;/li&gt;
&lt;li&gt;technique;&lt;/li&gt;
&lt;li&gt;depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The run improved until its reward reached roughly 0.65, then stopped making meaningful progress. Outputs converged on the same safe solution: a flat flower with five rounded petals. The score rose, but the capability did not.&lt;/p&gt;

&lt;p&gt;The problem was not a lack of feedback. It was too much duplicated feedback.&lt;/p&gt;

&lt;p&gt;The quality judges and prompt-adherence judge were correlated between 0.85 and 0.95. Five labels appeared to describe five qualities, but they were largely measuring the same underlying impression. Their combined weight counted one opinion several times. Meanwhile, the code-length reward saturated early and stopped producing a useful gradient. HPSv3 was the signal with visible variation, yet it carried only 10% of the reward.&lt;/p&gt;

&lt;p&gt;This is a common failure mode in composite evaluation. A long rubric feels safer because it names every concern. If the signals move together, however, adding them does not add information. It only amplifies a hidden preference. Reward dashboards can look richly instrumented while the optimizer receives the equivalent of one repeated instruction.&lt;/p&gt;

&lt;p&gt;There was another trap: a metric can remain numerically present after it has stopped teaching. Once nearly every rollout satisfies a binary gate or length target, that component contributes no useful distinction inside the batch. A saturated reward is bookkeeping, not learning signal.&lt;/p&gt;

&lt;p&gt;The diagnosis required inspecting each sub-reward separately: its variance over time, its correlation with other signals, and whether changes in it corresponded to visible capability changes. The total score alone concealed the collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairwise judgment gives taste a usable scale
&lt;/h2&gt;

&lt;p&gt;The original visual judges assigned absolute scores. In principle, “rate this from zero to ten” gives a clean scalar. In practice, model judges often compress their answers into a narrow region or apply the scale inconsistently. Is a competent synthetic watercolor a four, a six, or an eight? The number has no stable anchor.&lt;/p&gt;

&lt;p&gt;Pairwise comparison asks an easier question: &lt;strong&gt;which image better matches the target watercolor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every rollout, the system samples two reference paintings and asks a judge to choose. The rollout’s reward is the fraction of comparisons it wins. The task is still subjective, but the decision has context. Instead of inventing the meaning of seven out of ten, the judge distinguishes one concrete artifact from another.&lt;/p&gt;

&lt;p&gt;This changes the geometry of the feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reference set anchors the meaning of “good.”&lt;/li&gt;
&lt;li&gt;Close comparisons expose small improvements that an absolute scale may hide.&lt;/li&gt;
&lt;li&gt;Win rates spread results across a wider dynamic range.&lt;/li&gt;
&lt;li&gt;The team can audit disagreements by looking at the exact images shown to the judge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pairwise preference is also a natural fit for HPSv3. The &lt;a href="https://arxiv.org/abs/2508.03789" rel="noopener noreferrer"&gt;HPSv3 paper&lt;/a&gt; trains its preference model from pairwise annotations and uses an uncertainty-aware ranking loss. That does not make it a universal taste oracle. It makes the model useful as one broad prior about human image preference, which can then be balanced with project-specific references.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reference pool is taste made operational
&lt;/h2&gt;

&lt;p&gt;The team generated 1,664 candidate paintings and rated each one into three buckets: &lt;strong&gt;love&lt;/strong&gt; , &lt;strong&gt;okay&lt;/strong&gt; , or &lt;strong&gt;nope&lt;/strong&gt;. The 117 love-tier examples became the initial taste anchor. Additional acceptable images and supplemental generations expanded coverage, producing a pool of 581 references.&lt;/p&gt;

&lt;p&gt;Those numbers reveal how much human work sits behind a supposedly automated reward. The system did not discover the target aesthetic from the words “good watercolor.” Someone selected examples, rejected others, noticed gaps in color coverage, and decided which variations belonged together.&lt;/p&gt;

&lt;p&gt;The reference pool performs several jobs at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It turns personal taste into examples rather than an abstract instruction.&lt;/li&gt;
&lt;li&gt;It gives the judge a consistent comparison context.&lt;/li&gt;
&lt;li&gt;It reveals missing regions, such as colors with too few strong examples.&lt;/li&gt;
&lt;li&gt;It can be revised without relabeling every future rollout.&lt;/li&gt;
&lt;li&gt;It creates an auditable record of what the reward is trying to preserve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also creates bias. Every reference was model-generated because suitable human-made p5.brush work was difficult to source. The policy is therefore learning to outperform a synthetic neighborhood selected by one curator, not learning an independent definition of watercolor quality. If the pool overrepresents centered flowers or clean backgrounds, the model may learn those conventions as taste.&lt;/p&gt;

&lt;p&gt;The next natural step would be to train a smaller reward model directly on the ratings. That could generalize the curator’s preferences beyond repeated comparisons with a fixed pool. But it would not remove the design problem. It would encode the same choices into weights, making dataset balance, calibration, and held-out evaluation even more important.&lt;/p&gt;

&lt;h2&gt;
  
  
  The revised reward is smaller and more informative
&lt;/h2&gt;

&lt;p&gt;After the diagnosis, nine components became four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compilation and verified p5.brush use: &lt;strong&gt;5%&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;code-length check: &lt;strong&gt;5%&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;HPSv3: &lt;strong&gt;30%&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;pairwise judgment against the curated pool: &lt;strong&gt;60%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;/p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Ftraining-ai-to-paint-with-code%2Frevised-reward-weights.svg%25E2%2580%259D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/%25E2%2580%259C%2Fblog%2Ftraining-ai-to-paint-with-code%2Frevised-reward-weights.svg%25E2%2580%259D" alt="“A" width="800" height="400"&gt;&lt;/a&gt;Hard gates keep outputs runnable; most of the gradient now comes from visual preference.   &lt;p&gt;&lt;/p&gt;

&lt;p&gt;With the same base model and training data, the revised run reached the earlier plateau about three times faster and continued improving beyond it. Generated programs also shrank from roughly 13,500 tokens to fewer than 2,000. Once verbose code stopped being rewarded heavily, the model found that good compositions could be expressed more directly.&lt;/p&gt;

&lt;p&gt;The result is not an argument that every creative reward should use these exact weights. It demonstrates a method for finding useful weights:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep correctness checks as gates with enough weight to prevent invalid work.&lt;/li&gt;
&lt;li&gt;Measure the variance of every soft reward inside actual rollout groups.&lt;/li&gt;
&lt;li&gt;Compute correlations so duplicated judges do not impersonate independent evidence.&lt;/li&gt;
&lt;li&gt;Inspect examples at low, middle, and high scores.&lt;/li&gt;
&lt;li&gt;Remove or reduce signals that saturate before capability improves.&lt;/li&gt;
&lt;li&gt;Re-run with one major change at a time so the cause of improvement remains legible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A compact reward is easier to reason about. Every component has a distinct responsibility: executability, economy, broad human preference, and project-specific taste.&lt;/p&gt;

&lt;h2&gt;
  
  
  GRPO learns from relative outcomes within a group
&lt;/h2&gt;

&lt;p&gt;The optimizer used Group Relative Policy Optimization, or GRPO. Introduced in &lt;a href="https://arxiv.org/abs/2402.03300" rel="noopener noreferrer"&gt;DeepSeekMath&lt;/a&gt;, GRPO samples several outputs for the same input and estimates each output’s advantage relative to the group rather than training a separate value model.&lt;/p&gt;

&lt;p&gt;In this setting, one prompt produces several candidate sketches. They are rendered and rewarded. A sketch that performs better than its siblings receives positive relative advantage; one that performs worse receives negative advantage. The policy update then makes the better token trajectories more likely while limiting how far the new policy moves from the previous one.&lt;/p&gt;

&lt;p&gt;That relative structure pairs well with the project’s judging strategy. Both stages avoid pretending there is an absolute aesthetic truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the image judge compares a rollout with concrete references;&lt;/li&gt;
&lt;li&gt;GRPO compares a rollout’s reward with other rollouts from the same prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction matters. Pairwise judging defines the scalar reward. GRPO decides how that scalar changes the language model. It does not solve reward design. If the judge favors flat clip art, GRPO will efficiently train the policy to make flat clip art.&lt;/p&gt;

&lt;p&gt;Group-relative learning also needs diversity. If every candidate earns the same reward, the group provides little advantage signal. If one mechanical constraint dominates every comparison, the model learns that shortcut. Good rollout sampling and informative reward variance are part of the algorithm’s practical input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system prompt improved when documentation disappeared
&lt;/h2&gt;

&lt;p&gt;Reward design was only half the problem. The model also needed to know how to use a niche graphics library.&lt;/p&gt;

&lt;p&gt;An early system prompt included about 400 lines of p5.brush API documentation. Frontier models responded with polished JavaScript that confidently called methods the library did not have. More reference material increased the surface area for plausible invention.&lt;/p&gt;

&lt;p&gt;The team used &lt;a href="https://arxiv.org/abs/2507.19457" rel="noopener noreferrer"&gt;GEPA&lt;/a&gt;, an evolutionary prompt optimizer, to search for a better instruction. GEPA evaluates candidate prompts, reflects on execution traces and failures, proposes textual mutations, and preserves candidates that perform well across the evaluation set. After 200 iterations against a taste-anchored seven-example judge, the successful prompt was much smaller: a strict allowlist of eight brush methods, with no long API reference and no examples.&lt;/p&gt;

&lt;p&gt;The first version that reliably produced visible hibiscus forms arrived after most of the documentation was removed.&lt;/p&gt;

&lt;p&gt;This does not mean documentation is generally harmful. It means a generative model does not use an API manual like a compiler. Long context can mix authoritative names with nearby patterns and invite completion by analogy. For a constrained tool task, a small executable vocabulary may be more reliable than a broad descriptive reference.&lt;/p&gt;

&lt;p&gt;A practical tool prompt should therefore distinguish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;allowed operations&lt;/strong&gt; , listed exactly;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;forbidden fallbacks&lt;/strong&gt; , such as drawing with native p5.js when p5.brush is required;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;runtime invariants&lt;/strong&gt; , including canvas setup and completion behavior;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;output contract&lt;/strong&gt; , such as one complete sketch and no prose;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recovery behavior&lt;/strong&gt; , for example simplifying after an execution failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else can be retrieved only when needed or exposed through typed wrappers. The goal is not to teach the model the whole library. It is to make the valid action space obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can generalize beyond watercolor
&lt;/h2&gt;

&lt;p&gt;The experiment suggests a reusable design for subjective, tool-mediated work:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Keep the artifact editable
&lt;/h3&gt;

&lt;p&gt;Generate a structured source whenever the source is itself useful: HTML and CSS for layouts, vector instructions for diagrams, CAD operations for objects, MIDI for music, shader code for visuals, or node graphs for compositing. A rendered preview remains essential, but it should not erase the underlying decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Separate validity from quality
&lt;/h3&gt;

&lt;p&gt;Use deterministic checks for properties that genuinely are deterministic. The program compiles, calls allowed methods, finishes within limits, and produces a non-empty render. Do not ask an aesthetic model to guess those facts.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Prefer comparisons for ambiguous qualities
&lt;/h3&gt;

&lt;p&gt;When an absolute scale is unstable, compare concrete artifacts. References make the intended standard visible and reviewable. Pairwise evaluation is slower, but it often creates a cleaner signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Treat curation as model design
&lt;/h3&gt;

&lt;p&gt;The examples in a reference set shape the learned behavior as directly as the optimizer does. Track who selected them, which regions are overrepresented, and which failure modes are missing. Version the pool alongside the training configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Audit reward information, not just reward count
&lt;/h3&gt;

&lt;p&gt;More metrics are not automatically more supervision. Measure variance, correlation, saturation, and agreement with human review. Delete signals that merely repeat stronger ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Evaluate the entire pipeline
&lt;/h3&gt;

&lt;p&gt;The policy, prompt, tool API, renderer, judge, references, and optimizer form one system. A blank image may be a policy failure, an API hallucination, a browser crash, or a capture bug. End-to-end traces are more useful than a single final score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real creative medium is the feedback system
&lt;/h2&gt;

&lt;p&gt;The most interesting artifact in this project is not one hibiscus. It is the machinery that decides which hibiscus teaches the model.&lt;/p&gt;

&lt;p&gt;Creative reinforcement learning cannot avoid taste by adding more judges. Someone still chooses the rubric, the references, the comparisons, and the failure boundaries. The engineering achievement is to make those choices explicit enough to test: hard gates for execution, a broad preference prior, a curated local standard, and relative updates across alternative programs.&lt;/p&gt;

&lt;p&gt;Painting with code is slower than asking a direct image model for pixels. It is also more open to human intervention. The user can edit the source, the researcher can inspect the action, and the team can revise the meaning of “better” without pretending it was objective all along.&lt;/p&gt;

&lt;p&gt;That is the larger lesson. For subjective model behavior, the reward is not a neutral measurement attached after the creative work. Designing the reward &lt;strong&gt;is part of the creative work&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://surya.website/rling-qwen-to-paint-with-code" rel="noopener noreferrer"&gt;Training AI to Paint with Code&lt;/a&gt;, Surya Narreddi’s project write-up, training results, and visual progression.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/acamposuribe/p5.brush" rel="noopener noreferrer"&gt;p5.brush&lt;/a&gt;, the natural-media drawing library used by the generated sketches.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2402.03300" rel="noopener noreferrer"&gt;DeepSeekMath&lt;/a&gt;, the paper that introduced GRPO.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2507.19457" rel="noopener noreferrer"&gt;GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning&lt;/a&gt;, the prompt-optimization method used to evolve the system instruction.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2508.03789" rel="noopener noreferrer"&gt;HPSv3&lt;/a&gt;, the human-preference image scoring model used in the revised reward.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md" rel="noopener noreferrer"&gt;Puppeteer sandbox guidance&lt;/a&gt;, important when rendering model-written browser code.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>software</category>
    </item>
  </channel>
</rss>
