Ask anything, or task an agent…
Explore inspiration
10 interesting stories served every morning and every evening.
Ask anything, or task an agent…
Explore inspiration
Today, we are introducing Kimi K3 — our most capable model. Kimi K3 is a 2.8T-parameter model built on our Kimi Delta Attention and Attention Residuals, with native vision capabilities and a 1-million-token context window. It is the world’s first open 3T-class model, designed for frontier intelligence across long-horizon coding, knowledge work, and reasoning.
While its overall performance still trails the most powerful proprietary models, Claude Fable 5 and GPT 5.6 Sol, Kimi K3 demonstrated frontier-level performance across our evaluation suite, consistently outperforming other tested models.
Kimi K3 is available today on Kimi.com, Kimi Work, Kimi Code, and the Kimi API. At launch, Kimi K3 will use max thinking effort by default, with low- and high-effort modes to be introduced in subsequent updates. We are currently working closely with inference partners and open-source maintainers to align technical details and ensure a reliable rollout across the ecosystem. The full model weights will be released by July 27, 2026. Further details on the architecture, training, and evaluations will be released alongside the Kimi K3 technical report.
An Open 3T-Class Model
Kimi K3 is the first open model to reach 2.8 trillion parameters. It marks the latest step in Kimi’s sustained push at the scaling frontier: for nine of the past twelve months, Kimi models have set the upper bound of open-model sizes.
Kimi K3 is built on Kimi Delta Attention (KDA) and Attention Residuals (AttnRes), two architectural updates designed to improve how information flows across sequence length and model depth. We have also scaled up Mixture of Experts (MoE) sparsity, effectively activating 16 out of 896 experts when paired with a Stable LatentMoE framework. Together with refined training and data recipes, these structural changes yield an approximate 2.5× improvement in overall scaling efficiency compared to Kimi K2, allowing the model to convert compute into intelligence more effectively.
Coding
Kimi K3 has strong long-horizon coding performance. Operating with minimal human oversight, it can sustain long engineering sessions, navigate massive repositories, and orchestrate terminal tools.
Kimi K3 also excels in tasks blending software engineering with visual reasoning — it leverages screenshots and visuals to optimize game dev, frontend, and CAD.
The case studies below show how Kimi K3′s coding capability translates into open-ended software creation and scientific research.
Kernel Optimization
We tested the models’ capability to optimize GPU kernels. Each model works independently in an identical sandbox, with up to 24 hours to profile, rewrite, and benchmark four tasks spanning AttnRes, KDA, and a 512-head-dimension MLA kernel across NVIDIA H200 and GPGPU from an alternative vendor. Kimi K3 performed competitively with Fable 5 (with fallback) and substantially outperformed Opus 4.8, GPT 5.6 Sol, and GPT 5.5.
Claude Fable 5 was evaluated by a third party, and its results may include fallback behavior. Across most models, some trajectories include small, acceptable precision shortcuts that remain within our numerical tolerance. GPGPU denotes general-purpose GPUs used for computation beyond graphics rendering.
In the late stages of Kimi K3 development, an early version of Kimi K3 handled the majority of the team’s kernel optimization works.
GPU Compiler Development
We further tested whether Kimi K3 could build a GPU programming system from scratch. Kimi K3 developed MiniTriton, a compact Triton-like compiler with its own tile-level IR layer over MLIR, optimization passes, and a PTX code-generation pipeline. Across supported roofline benchmarks, MiniTriton delivers performance on par with or better than Triton and torch.compile — beating Triton on certain workloads. Beyond microbenchmarks, MiniTriton sustains end-to-end nanoGPT training with stable convergence, the loss curve closely tracking the reference with only minor divergence — validating the full pipeline on a realistic workload. These results demonstrate that Kimi K3 can build a coherent end-to-end compiler — from DSL frontend and IR passes to PTX codegen and runtime — rather than isolated kernels; its from-scratch Tensor Core path already rivals Triton’s extensively optimized stack.
Game Dev and Digital Creation
Kimi K3 combines strong 3D reasoning, coding, and vision capabilities to turn concepts, images, and videos into fully playable interactive experiences. Kimi K3 achieves true “vision in the loop” by seamlessly iterating between code and live screenshots—instantly seeing and refining outputs.
Case 1: 3D Open World
Kimi K3 built a fully procedural browser-based 3D exploration game using Three.js WebGPU and GPU compute. It procedurally generated the environment, while using a 3D asset generation tool to create the rider and horse models, producing an expansive open world with forests, a log-cabin village, snowy mountains, and dynamic weather. External assets used: animated cowboy and horse models and terrain data.
Chip Design
As an early proof of concept, Kimi K3 designed a chip to serve a nano model built on its own architecture. In a single 48-hour autonomous run, K3 built, optimized, and verified the chip using open-source EDA tools on the Nangate 45nm library. Within 4 mm², the chip closes timing at 100 MHz and sustains over 8,700 tokens/s decode throughput in simulation, packing 1.46M standard cells, 0.277 MB of SRAM, and an INT4 MAC array with fused dequantization. A chip built by a model, for a model, reflects K3′s long-horizon agentic capabilities.
Coding for Research
Kimi K3 bridges scientific literature and executable code, autonomously implementing, validating, and analyzing complex computational research workflows.
In one case, Kimi K3 completed in about two hours what would typically require one to two weeks of work by an experienced researcher. To reproduce the I–Love–Q universal relations in computational astrophysics, it reviewed and cross-validated 20+ papers, implemented the full numerical pipeline, evaluated 300+ equations of state, identified inconsistencies in published formulas, generated 3,000+ lines of Python code, and produced an interactive HTML dashboard for exploring the results.
Knowledge Work
Kimi K3 advances end-to-end knowledge work. Beyond public benchmarks, Kimi K3 (max) demonstrates consistent gains across our internal evaluations, which are derived from recurring patterns and challenges observed in real-world user-agent workflows. These consistent advantages across distinct production-oriented workflows reflect a broad improvement in Kimi K3′s agentic knowledge work capabilities.
Research with Interactive Visualization
Below are a few examples of what Kimi K3 in Kimi Work can produce across financial consulting and scientific research:
Case 1: Interactive 42 years of AI ASIC industry research website
An interactive research report you can drill into: 42 years of the ASIC industry, created through 120+ rounds of recursive self-improvement. Kimi K3 transforms evidence into bespoke charts, animated diagrams, and interactive visual narratives. It pulled data via 2.8k+ web searches/fetches and 1.1k+ terminal data pulls, across 11k+ pages spanning 87 quarterly reports and 99 original PDFs.
Case 2: Fusion Industry Research
A consulting-style industry report with interactive visualizations—including timelines, Funnel Chart, Range Bar Chart, Gantt Charts, and publication-quality slides.
Case 3: GWTC-5 Gravitational-wave Analysis
An analysis of 391 gravitational-wave events using 20+ concurrent subagents, producing 7 scientific visualizations, 2 tables, and a literature synthesis from 10+ papers.
Kimi K3 is also particularly effective at producing infographic-style presentations, such as the fully editable heatmap and annual report shown below:
Widgets and Dashboard
In Kimi Work, we introduce two new features - Widgets and Dashboard - which make interactions with Kimi K3 more visual and persistent. Widgets let you generate interactive components directly within a chat, with connections to local data or external plugins for continuous updates. Dashboard brings the widgets you care about most into one persistent, personalized view organized around a topic, project, or goal.
Video Editing
Kimi K3 excels at motion design, animation, and video editing because its native multimodal architecture understands text, images, and video within the same model.
In one example, K3 created a 3Blue1Brown-style motion-graphics explainer of its own architecture, translating technical ideas into animated diagrams and transitions.
In another, Kimi K3 edited its own teaser video from 56 source clips, handling clip selection, motion-matched cuts, frame-accurate beat synchronization, audio processing, and multiple rounds of revision. A high-density short video like this would typically take an experienced editor one to two working days, or a beginner three to five.
Architecture and Infrastructure
Kimi K3 is built on Kimi Delta Attention (KDA) and Attention Residuals (AttnRes). KDA provides an efficient foundation for scaling attention, while AttnRes selectively retrieves representations across depth rather than accumulating them uniformly. Together, they form the architectural backbone of a model designed to scale well beyond the trillion-parameter regime.
Kimi K3 uses Stable LatentMoE, effectively activating 16 of 896 experts. At this level of sparsity, routing and optimization become first-order challenges. Quantile Balancing derives expert allocation directly from router-score quantiles, eliminating heuristic updates and a sensitive balancing hyperparameter, while Per-Head Muon extends Muon by optimizing attention heads independently for more adaptive learning at scale. Sigmoid Tanh Unit (SiTU) and Gated MLA improve activation control and attention selectivity respectively. Together, these advances enable stable and efficient training at the 2.8-trillion-parameter scale.
Kimi K3 applies quantization-aware training from the SFT stage onward, using MXFP4 weights with MXFP8 activations for broad hardware compatibility. To prevent expert imbalance from degrading throughput at large expert-parallel scales, we introduce a fully balanced expert-parallel training method with static shapes and no host synchronization on the critical path. Since inference efficiency likewise benefits from larger high-bandwidth communication domains, we recommend deploying Kimi K3 on supernode configurations with 64 or more accelerators. Finally, as KDA poses new challenges for conventional prefix caching, we have contributed a corresponding implementation to the vLLM community, to be released alongside the model. KDA with prefill cache allows us to serve Kimi K3 at a highly competitive token price despite its scale and long context.
More technical details will be available in our coming report.
Availability
Kimi K3 Agents: Download or update to the latest Kimi app from your mobile app store, available on iOS, Android, and HarmonyOS, or visit kimi.com.
Work with Kimi K3: Download the latest Kimi Work desktop app, version 3.1.0 or later, available for Windows and Apple silicon Macs.
Code with Kimi K3: Run Kimi Code in your terminal and select Kimi K3 using the /model command.
Build with the Kimi API: Visit the Kimi API Platform and select kimi-k3. Pricing is $0.30/MTok for cache-hit input, $3.00/MTok for cache-miss input, and $15.00/MTok for output. Powered by Mooncake’s disaggregated inference architecture, the official Kimi API achieves a cache hit rate above 90% in coding workloads.
Bring Kimi to your organization: Kimi Enterprise provides enterprise-grade data privacy and member management, with complete separation between personal and organization accounts. Visit the pricing page and select “Get Kimi Enterprise” to subscribe for your team.
Full Benchmark Table
Footnotes
All Kimi K3 results reported below are obtained with the reasoning effort set to ‘max’, setting temperature = 1.0 and top-p = 1.0. Depending on the benchmark, each model is evaluated under one of three agentic harnesses — KimiCode, Claude Code, or Codex — as specified in the notes below.
Coding benchmarks
DeepSWE. Kimi K3 is evaluated with the KimiCode harness. The GLM-5.2 score is taken from the GLM-5.2 release blog (https://z.ai/blog/glm-5.2); all remaining scores are from the official DeepSWE leaderboard (https://deepswe.datacurve.ai/), under which Kimi K3 attains 67.3 with the mini-SWE-agent harness.
Terminal-Bench 2.1. Kimi K3 is evaluated with the KimiCode harness. For all other models, we report the best score across harnesses: GLM-5.2 with Claude Code (https://z.ai/blog/glm-5.2); Claude Opus 4.8 and Claude Fable 5 with Terminus 2 (https://artificialanalysis.ai/evaluations/terminalbench-v2 – 1); GPT 5.5 and GPT 5.6 Sol with Codex (https://openai.com/index/previewing-gpt-5 – 6-sol/).
Program Bench. Kimi K3 is evaluated with the KimiCode harness. The GLM-5.2 score is from https://z.ai/blog/glm-5.2; all other scores are from https://www.vals.ai/benchmarks/programbench.
SWE Marathon. Kimi K3, Claude Opus 4.8, and Claude Fable 5 are evaluated with the Claude Code harness; GPT 5.6 Sol is evaluated with the Codex harness. The GLM-5.2 score is from https://z.ai/blog/glm-5.2.
FrontierSWE. Kimi K3 is evaluated with the KimiCode harness and GPT 5.6 Sol with the Codex harness; all other results are from https://www.frontierswe.com/. Dominance scores are recomputed from the raw scores using the official evaluation script and are current as of July 16, 2026.
PostTrain Bench. Scores for GLM-5.2, GPT 5.5, and Claude Opus 4.8 are adopted from the official PostTrainBench results. Kimi K3, Claude Fable 5, and GPT 5.6 Sol are evaluated with the official Harbor implementation at maximum reasoning effort, averaged over three runs — Kimi K3 and Claude Fable 5 with the Claude Code harness, and GPT 5.6 Sol with the Codex harness. Under the Claude Code harness, requests refused by Claude Fable 5 due to its usage policy automatically fall back to Claude Opus 4.8.
MLS Bench Lite. Kimi K3 is evaluated with the KimiCode harness; GLM-5.2 and the Claude models with the Claude Code harness; GPT 5.5 and GPT 5.6 Sol with the Codex harness.
KCB 2.0. Kimi K3 is evaluated with both the KimiCode and Claude Code harnesses; GLM-5.2, Claude Opus 4.8, and Claude Fable 5 with the Claude Code harness; GPT 5.5 and GPT 5.6 Sol with the Codex harness. All models are evaluated at maximum reasoning effort, except GPT 5.5, which uses the “xhigh” setting.
Productivity and agentic benchmarks
OfficeQA Pro and SpreadsheetBench 2. Kimi K3, GLM-5.2, Claude Opus 4.8, and Claude Fable 5 are evaluated with the Claude Code harness; GPT 5.5 and GPT 5.6 Sol are evaluated with the Codex harness.
MCP Atlas. All models are evaluated on the 500-task public subset with a 100-turn limit, using Gemini 3.1 Pro as the judge.
AutomationBench. All models are evaluated on the 600-task public subset, following the official GitHub setup in all other respects.
BrowseComp. We adopt the context-compaction strategy used in the Claude model cards, triggered at 300K tokens. When evaluated with a 1M-token context window and no context management, Kimi K3 achieves a score of 90.4. The results of Claude Fable 5, Claude Opus 4.8, GPT 5.6 Sol, and GPT 5.5 are cited from https://www.anthropic.com/news/claude-fable-5-mythos-5 and https://openai.com/index/gpt-5 – 6/.
GDPval-AA v2 and AA-Briefcase scores are cited from https://artificialanalysis.ai/.
Multimodal benchmarks
Except for ZeroBench, which follows the official setting and is run five times, all multimodal scores are averaged over three runs. MMMU-Pro is evaluated following the official protocol, preserving the original input order and prepending images to the text input.
PerceptionBench. PerceptionBench is an in-house benchmark that focuses on atomic visual perception capabilities.
Limitations
Sensitivity to thinking history. K3 was trained in the preserved thinking history mode. If the agent harness fails to pass back all the historical thinking content as required, or if an ongoing session with another model is switched over to K3, generation quality may become highly unstable. We recommend using a harness with verified compatibility, such as Kimi Code, and avoiding switching to K3 in the middle of a session.
Excessive proactiveness. K3′s training places particular emphasis on long-horizon, challenging tasks. As a result, when it encounters minor issues or ambiguous user intent during task execution, it may make unexpected decisions on the user’s behalf. If your application requires the agent to operate within well-defined boundaries and refrain from excessive improvisation, please impose more explicit behavioral constraints on K3 in the system prompt or in AGENTS.md.
Despite being a highly competitive model overall, K3 nonetheless exhibits a noticeable gap in user experience compared with Claude Fable 5 and GPT 5.6 Sol.
Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening. Pigeons & Planes is all about music discovery, supporting new artists, and delivering the best music curation online and IRL. We’re always listening.
from the poof-it’s-gone dept
In all of our discussions about how the digital revolution has created a system in which people don’t actually own the things they think they’re buying, I get particularly frustrated by the lack of change in it all. We’ve spilled much ink complaining that this clearly anti-consumer practice needs to be done away with, where an unsuspecting public thinks they’re buying “a thing” only to learn months or years later that “the thing” they bought was actually a license to use/view/listen to another “thing”, and that license exists at the pleasure of the company that collected the money for it. And if you want to see the lack of change or action really honed in upon, let’s take a look at Sony’s PlayStation Store.
In 2022, due to “evolving licensing agreements” with distributor StudioCanal, German and Austrian users had hundreds of movies disappear from their PS accounts, long after buying them through Sony. Then in 2023, it happened again in America, specifically when Sony ended its licensing agreement with Discovery after the Warner Bros. merger, which, of course, has since been bought by Paramount Skydance. That resulted in customers having hundreds and hundreds of episodes of TV shows deleted from their accounts. Nowhere in any of this were there refunds, of course. No recompense at all, actually. Just a thing you thought you’d bought taken away from you by the very people you thought you bought it from.
And now it’s happening again. Due to another licensing agreement fallout with StudioCanal, hundreds of movies and TV shows are being ripped from the accounts of PS Store customers, and there appears to be fuck all that they can do about it.
This news was brought to people’s attention by X user somatyk, who posted the notification they had received from PlayStation this week. Along with the unapologetic news that the purchased movies would be deleted from their account on September 1, the message concluded with, “Click here for a full list of affected titles that will no longer be supported. Thank you.” The same warning is now reproduced in full on the PlayStation website, along with the list of 551 films and TV series that are being pulled from people’s libraries.
This news was brought to people’s attention by X user somatyk, who posted the notification they had received from PlayStation this week. Along with the unapologetic news that the purchased movies would be deleted from their account on September 1, the message concluded with, “Click here for a full list of affected titles that will no longer be supported. Thank you.” The same warning is now reproduced in full on the PlayStation website, along with the list of 551 films and TV series that are being pulled from people’s libraries.
As Kotaku notes later in their post, part of what is striking in all of this is the sheer mundanity of the announcement. Because there have been no consequences, or any action at all from the public or government, Sony treats this all as if it’s perfectly normal and no big deal. You can tell me all you want about how the Ts and Cs in these purchases do in fact note that the nature of the purchase is a temporary licensing of the content for an undetermined time period… but I can promise you that the public in general doesn’t understand that. They think they’re buying a thing, not a license.
And that’s because of the purposeful obfuscation of that fact. Sony damned well knows that the vast majority of people don’t read those Ts and Cs. It knows that the public largely doesn’t understand how these backend licensing agreements with distributors work, or that they even exist. And Sony isn’t exactly putting out a big blinking sign on its store pages informing the public of all of this. Instead, the company is only too happy to collect money from a public that is being purposefully kept ignorant of what they’re buying.
Of course, when you scroll past the endless EULAs when you first use your PlayStation, and click “Agree” the first time you load the store, you’re unwittingly agreeing that nothing you buy is really truly bought, and that it can be taken away from you at any point, and there’s nothing you can do. The same is true of your games.
Of course, when you scroll past the endless EULAs when you first use your PlayStation, and click “Agree” the first time you load the store, you’re unwittingly agreeing that nothing you buy is really truly bought, and that it can be taken away from you at any point, and there’s nothing you can do. The same is true of your games.
This, too, will probably pass without any real action. The government has done its best to gut our consumer protection agencies, so they won’t be any help. Angry customers won’t coalesce into activism or action, most likely. And I’ll probably be writing another one of these posts in a couple of years when it all happens again.
But it shouldn’t be that way. There are common sense things that can be done to better inform the public. Rules for how the store should inform people with each and every purchase. Someone just needs to demand it be done.
Filed Under: eula, ownership, playstation, playstation store, video games
Companies: sony, studiocanal
The chat client that brought Comic Sans to the world is now on GitHub
Today, we’re excited to announce the open-source release of Microsoft Comic Chat, the chat client that automatically turned conversations within Internet Relay Chat (IRC) into comic panels featuring illustrated characters, speech bubbles, and expressions, and helped introduce the world to a little font called Comic Sans.
Yes, that Comic Sans. Originally designed by Microsoft typographer Vincent Connare in 1994, Comic Sans found its first real home in Comic Chat, where its informal, hand-lettered feel matched the software’s speech-bubble conversations perfectly.
For many people, Comic Chat is a nostalgic artifact from the early days of the internet as we transitioned from technologies like telnet, Usenet, and IRC to the largely visual web that we enjoy today. For others, it’s a legendary piece of Microsoft history they have only heard about in stories, screenshots, and debates about typography. Now, developers, historians, retro computing enthusiasts, and anyone who appreciates a wonderfully unconventional idea can explore the source code for themselves.
A different vision for online communication
Today we’re accustomed to messaging apps with reactions, stickers, GIFs, avatars, video, and AI-generated content. But in the mid-1990s, internet chat was largely walls of scrolling text.
Rather than displaying messages as plain text, Comic Chat presented participants as illustrated characters. Conversations unfolded in comic panels, with speech bubbles, expressions, and gestures generated from what people typed. If someone wrote “I like that,” the character might point to itself. If the text suggested anger, the character might frown or cross its arms. It was quirky, ambitious, occasionally chaotic, and surprisingly forward-looking.
Many ideas we now take for granted in online communication can trace some of their spirit to experiments like Comic Chat.
The people who built it
David “DJ” Kurlander, working in the Microsoft Research Virtual Worlds Group, conceived the idea of a new visual representation of conversational histories, and started developing Comic Chat in 1995. Built in Visual C++ 4.0 and MFC, Comic Chat was released in 1996 with the Internet Explorer 3 web browser.
Under the hood, Comic Chat was more than a clever skin for IRC. It was able to interpret conversational cues in the text and choose appropriate poses, facial expressions, gestures, and panel layouts. That meant Comic Chat was not simply displaying messages but also making real-time editorial decisions about how a conversation should look and feel as a comic. DJ, Tim Skelly, and David Salesin published a paper on the technology in Comic Chat at SIGGRAPH ’96, a computer graphics conference, describing what they had built as an experiment in automatic illustration construction and layout.
The visual world of Comic Chat was the work of Jim Woodring, a highly regarded independent comic artist whose characters gave the software its distinctive look. The team would hand Jim transcripts of real chat sessions to illustrate, then use the results to figure out whether the whole idea was worth pursuing. It was.
Why open source it now?
Comic Chat represents a fascinating chapter in the evolution of online communication. It emerged during a period when the internet was still discovering what it wanted to become. Many rules had not yet been written, which gave developers permission to try bold concepts that might seem unusual even today.
By releasing Comic Chat as open source, we’re preserving an important piece of software history and giving the community an opportunity to explore, learn, and build upon it.
The source is available now for exploration, study, and experimentation. Alongside the original snapshots, we’ve included a few AI-powered modernization attempts that demonstrate what’s possible—getting this 1990s-era C++ and MFC code building with current Visual Studio tools, connecting to modern IRC servers, and running legibly on today’s high-resolution Windows machines. These are not polished re-releases, but worked examples that show Comic Chat can still come alive on modern systems. We’re excited to see what improvements, ports, experiments, and entirely new forms the community brings to it next.
A time capsule of internet optimism
Looking back, Comic Chat captures something special about the era in which it was created.
The early web was filled with experimentation. “What if chat rooms looked like comics?” That question sounds wonderfully unreasonable. And yet it was built, shipped, localized into 24 languages, and bundled with Windows 98.
That’s part of what makes Comic Chat memorable decades later. It reminds us that innovation often starts with ideas that are playful, unconventional, and creative.
One last speech bubble
Comic Chat was created during a period when software teams were willing to color outside the lines, literally and figuratively. DJ Kurlander, Tim Skelly, David Salesin, Jim Woodring, and everyone else who touched this project made something that people still remember and still run thirty years later.
Take a look at the source code, explore what they built, and use its story as inspiration to come up with new unconventionally delightful things to create.
And if you happen to read the source code in Comic Sans, we promise not to judge.
For the past year and a half, the team building Roc’s compiler has been rewriting our 300,000 lines of Rust code into Zig, for reasons I’ll recap below. We recently passed an exciting milestone: feature parity with the original compiler!
Since the Bun project recently shared an experience report of their rewrite in the other direction (from Zig to Rust, although that’s only the tip of the iceberg of differences between our rewrites), this seems like a nice time to reflect on how our move from Rust to Zig is going.
Passing Feature Parity
Hitting this milestone made it possible to update Brendan Hansknecht’s charming 2024 WASM-4 game, Rocci Bird (with art by Luke DeVault) to use the new compiler. It’s a nice example because the whole game is under a thousand lines of Roc code, and you can play it on itch.io or right here via WebAssembly:
Click or tap the game, then press Space (or tap) to flap. On mobile you don’t have a right arrow key, so refresh the page to restart the game.
Rocci Bird’s updated source code is a bit more concise than the original, and roc build –opt=size now outputs a 31KB wasm binary. (The original compiler produced a binary more than double that size.) Rocci Bird is by no means a large code base, but getting it to run at all required landing a lot of features in the new compiler. Seeing those chunky purple pixels brought a smile to my face when we finally got there!
To be clear, this is a milestone but not a formal release. (We aim to land version 0.1.0 later this year.) That said, it’s a wonderful milestone to have reached, and I’m extremely grateful to all the people who came together to make this happen! I want to thank some in particular who have been especially helpful in getting the language and compiler to this point:
Anthony Bullard and Sam Mohr for collaborating on the new parser
Jared Ramirez for the new type-checker (among many other things!)
Ayaz Hafiz for the new lambda set resolution system, plus tons of the original compiler
Aurélien Geron for hand-updating 108 (!) beginner exercises in the Roc Exercism course he originally created
Stephan for getting the compiler’s new “echo” platform running in the browser, so that anyone can now write and run basic Roc programs from the roc-lang.org homepage via a 2.5MB WebAssembly binary!
Niclas Åhdén, Roc’s most prolific production user, for patiently filing helpful bug reports and giving actionable feedback about the upgrade process
JRI98 for methodically reproducing and investigating fuzzer errors and other bugs, closing out issues that no longer reproduced, and more
Jasper Woudenberg for iterating on API designs for userspace packages using the new compiler
Folkert de Vries, Brendan Hansknecht, Brian Carroll, Josh Warner, Agus Zubiaga, and Jelle Teeuwissen for building the foundation of the original compiler, without which the new compiler never would have existed
I’ve saved the undisputed biggest contributors to the new compiler for last: Anton-4 and Luke Boswell for so many things I can’t even keep track of them all—compiler work, builtins, platforms, packages, examples, fixing bugs, helping beginners on Roc Zulip…enumerating it all could take up a whole second post! It’s been incredible seeing how much you’ve built.
Thank you all so much! I feel honored that you’ve put so much of your valuable time into this project. Also thanks to our past and present sponsors—rwx, Lambda Class, ohne-makler, martian, tweede golf, Vendr, NoRedInk, and many generous individual sponsors—who have helped get us to this point by supporting our contributors.
Speaking of time: our 487-day rewrite took 476 days longer than Bun’s 11-day rewrite from their ~500K lines of Zig into Rust. There are many reasons for this difference which have nothing to do with Rust or Zig, including the fact that theirs was a direct port whereas we’d decided to rewrite because of how much we were going to change. The techniques they used wouldn’t have worked in our case.
The laundry list of changes we made also means comparing our original Rust code base and new Zig code base won’t be apples-to-apples. Still, we’ve reached a nice point to reflect on how the rewrite has gone, both in terms of what new features it has unlocked for Roc programmers, as well as how our experiences with Rust and Zig have compared.
Let’s get into it!
Hot Code Loading + Cross-Compiled Binaries
Roc’s new compiler automatically does hot code loading during development. For example, I can run roc server.roc to start a Web server, then change some of its code while it’s running. The next time that server handles a request, it’ll automatically be handled using the new code. Here it is in action, both in a server and in a simple 2D game:
Hot loading is standard behavior for interpreted languages like Python, but not so much for high-performance compiled languages like Roc. When I’m ready to deploy, roc build server.roc gets me an LLVM-optimized, self-contained binary that I can drop onto a machine and run.
Roc also cross-compiles; building a static binary that runs on Alpine Linux is as simple as roc build –target=x64musl, and that command will produce the same output bytes (for the same input source code bytes) when run on a Mac or any other system—which not all compilers guarantee.
Pattern Matching with String Interpolation
The HTTP request-handling logic from that video looks like this:
match (verb, path) { (“GET”, “/users/${id}/${page}“) => match page { “” | “profile” => ok(id) “settings” => ok(with_default(user_agent, id)) “posts/${post_id}” => ok(“Post ID: ${post_id}“) _ => not_found }
(“GET”, “/users/${id}“) => ok(id)
(“POST”, “/posts/new”) => created(with_default(…))
_ => not_found }
This uses several features we introduced in the new compiler. For example, that “/users/${id}” syntax is not implemented with parsing template strings at runtime, but rather with a new language feature: string interpolation inside pattern matching.
Not only is this type-safe at compile time, this entire code snippet performs zero heap allocations. I’d expect the typical language that ships with hot code loading to average closer to 1 allocation per line of code here…but Roc is aiming high on ergonomics, type safety, and performance!
You can play around with this syntax on the new roc-lang.org homepage - if you scroll down a bit, there’s an WebAssembly build of the compiler right there on the page that you can use to try out the language.
By the way, if you’re interested in a post on the technical details of how we used the new compiler’s compile-time execution of pure functions to get HTTP request routing down to zero allocations, let me know on Roc Zulip.
By the way, if you’re interested in a post on the technical details of how we used the new compiler’s compile-time execution of pure functions to get HTTP request routing down to zero allocations, let me know on Roc Zulip.
Why a Scratch-Rewrite?
Unlike Rust, C, and Zig, Roc is not a systems language; it has automatic memory management (using reference counting, both to avoid tracing collector pauses and also for Perceus optimizations and opportunistic mutation like Koka’s). Roc would have way more heap allocations if it needed one heap allocation per closure capture (like most non-systems languages do), but our closure captures don’t heap-allocate because Roc is the first non-academic language to implement polymorphic defunctionalization through lambda set specialization.
This might sound like a niche optimization, but in a functional language like Roc, defunctionalization turns out to be similar to inlining in that it unlocks a treasure trove of follow-up optimizations. Although this system proved incredibly beneficial to Roc’s runtime performance, it also proved incredibly difficult for us to implement correctly. We struggled with nasty bugs in the original implementation, and only after Ayaz Hafiz prototyped a new architecture in OCaml were we able to finally get it right in the new compiler.
Ayaz’s prototype showed that the root of our problems was architectural across several compiler phases, and fixing it would require rewriting most of the compiler. This was one reason we decided to rewrite in the first place—that, and several contributors independently mentioning they planned to rewrite various parts of the compiler for other reasons. We realized we were about to rewrite almost all of the compiler anyway, so it made sense to consider a full rewrite as an alternative to the Ship of Theseus approach.
Compilers are unusual in that scratch-rewrites are the norm among successful projects. It’s often the only way to self-host, although not all compilers rewrite into their own language; see for example TypeScript’s rewrite to Go. My position has always been that Roc’s compiler should not self-host, so the idea that someday the benefits of a rewrite might seem to outweigh their notorious costs had frankly never occurred to me.
The more we talked about it, the more sense it made to do what basically every mainstream compiler today has done at some point: rewrite from scratch.
Why Zig?
Once we’d decided to scratch-rewrite, the next question was whether to choose Rust again. Based on our experiences with both Rust and Zig (we were already using Zig for a bunch of primitives in our standard library), we decided to build the entire compiler in Zig this time.
I enjoy Rust, I’ve taught a course on it, and I happily use it daily for my work at Zed. Despite what Internet comments might have us believe, it’s extremely normal for one language to be the best fit for one project, while a different language turns out to be the best fit for a different project. One size does not actually fit all!
I’ve talked in depth about our reasons for going with Zig elsewhere—in writing, on podcasts, and so on—and we only seriously considered Rust and Zig, because those were the only systems languages our team knew well enough. The biggest considerations on our minds when deciding between Rust and Zig were:
Build times. Our cargo build times were a major pain point, even for incremental builds, and getting worse as our code base grew. We expected build times in a Zig rewrite to be much faster.
Memory control. We use a variety of different memory allocators throughout compilation, especially arenas, and struct-of-arrays layouts all over the place. Rust’s ecosystem consistently assumes one global allocator, including soa_rs. Zig’s whole ecosystem assumes granular allocators, and struct-of-arrays support is standard.
Ecosystem relevance. Rust’s ecosystem is much bigger than Zig’s overall…but almost no packages in either ecosystem are relevant to our particular needs. For the niche things we wanted to get off the shelf—such as a faster way to emit LLVM bitcode than wrapping LLVM’s C++ library—more of that code existed in Zig than in Rust.
Memory-unsafety assistance. Rust is designed to isolate memory-unsafe code inside rare unsafe blocks, and use things like miri or Valgrind to vet those. Memory-unsafe code wasn’t rare for us, though (more on this later) and we ended up with about 1,200 uses of unsafe (out of our 300K lines of Rust code; compare to about 40,000 uses of unsafe in rust’s 3.5M lines, and remember that for compilers which emit machine code, like roc and rustc, doing memory-unsafe things is a big part of the job). Zig has more features than Rust for making memory-unsafe code work correctly, and that was the area where we wanted the most help.
After a year and a half of rewriting, how did our expectations of Zig’s benefits line up with the reality of what we got? And which parts of Rust did we end up missing once we no longer had access to them?
Life Without Borrow-Checking
Let’s start with memory safety. There’s a famous 2019 Microsoft presentation that says, on slide 10:
~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues.
~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues.
The presentation’s next slide has a breakdown by type of memory safety issue, which paints the following picture when it comes to Rust and Zig specifically:
83.6% of vulnerabilities addressed through a security update in 2018 would have been completely unaffected by the choice of Rust or Zig, because both languages handle all of these scenarios (out-of-bounds reads/write, unsafe casts, uninitialized reads, stack overflows, and non-memory-safety issues) in the same way.
16.4% of the vulnerabilities were specifically use-after-free errors. These could have been caught by Zig’s ReleaseSafe runtime memory-safety checks, or Rust’s borrow checker, or the checks Fil-C uses…modern languages have a variety of ways to help catch UAFs, although these CVEs from 2018 would have almost certainly been from C or C++ code instead.
ReleaseSafe catches use-after-free errors through runtime checks which panic if the program tries to use freed memory. Compared to Rust’s safe subset, Zig’s checks are less comprehensive, have a runtime cost, and can panic. That said, Zig with ReleaseSafe has worked great in practice for the TigerBeetle database, which recently underwent a legendarily meticulous Jepsen report that found only two safety bugs, neither related to memory safety.
ReleaseFast skips these checks in production builds to avoid their overhead, but keeps them in debug builds and tests to catch memory-safety issues during development. If your tests covered every possible real-world code path, ReleaseFast would give you the same safety as ReleaseSafe, but that level of test coverage is rarely practical; the real question is what slips through the coverage cracks in practice. Bun talked about their struggles with use-after-frees, but other widely-used projects building with ReleaseFast have had no CVEs caused by memory unsafety in their Zig code. Ghostty is one, and Zig’s compiler itself is another.
If you want to learn more about these projects, I’ve recorded in-depth conversations with their creators: Joran Greef on TigerBeetle, Mitchell Hashimoto on Ghostty, and Andrew Kelley on Zig.
If you want to learn more about these projects, I’ve recorded in-depth conversations with their creators: Joran Greef on TigerBeetle, Mitchell Hashimoto on Ghostty, and Andrew Kelley on Zig.
Rust code has a different source of memory-safety gaps: the unsafe sections that nearly every Rust program has somewhere in its dependencies. Unsafe Rust has all the memory unsafety risk of ReleaseFast Zig code, but none of the runtime checks to catch issues during development. The Rust ecosytsem has miri to find bugs in non-FFI unsafe code, and Valgrind can help too, but few Rust projects use either. That said, the cultural norm of using unsafe rarely, and auditing it extra carefully, has worked out well enough to earn Rust a strong reputation for memory safety in practice.
Of course, Rust memory unsafety errors can and do still slip through the cracks. Deno, a Bun competitor which is written in Rust, has had memory-unsafety CVEs including an out-of-bounds read as well as a use-after-free, both involving the use of Unsafe Rust. Rocket, a Rust Web Framework, has had a use-after-free CVE, and Actix has had a variety of memory-unsafety CVEs from a period when its use of unsafe was abnormally high.
When we were deciding between Rust and Zig for the new compiler, we were aware of all of this. We knew Rust had a well-deserved reputation for memory safety, but that memory unsafety could still happen, and we’d experienced all of that firsthand with the original compiler. We also knew we’d be using unsafe way more than typical Rust projects, and even though we were already using Valgrind, getting help with innately memory-unsafe code from Zig’s additional checks sounded appealing. We wanted the hard stuff to get easier, and we weren’t worried about use-after-free issues in a compiler where allocations would be overwhelmingly done in arenas with straightforward lifetimes.
We knew high-profile Zig projects had achieved great performance and memory safety in practice, and we decided to aim for becoming another of those success stories.
Memory Safety Post-Rewrite
It’s easy to theorize about how things will go with a particular technology choice, but where the rubber meets the road is what end users encounter in real-world usage. So how has Zig with ReleaseFast worked out for us in practice? How many memory corruption incidents—from use-after-frees or any other cause—have we seen since rewriting our compiler from Rust to Zig?
Here’s a breakdown of bug reports in Roc’s issue tracker, as classified by Claude Opus 4.8:
You might be wondering how the Rust-based compiler had any memory corruption bugs at all, let alone more than double the total count of the Zig-based one. Is it because of that pesky Unsafe Rust again?
Actually, no. None of those 21 memory corruption bugs occurred in the compiler’s logic itself, which is a testament to Rust’s borrow-checker working as intended. The reason we had memory corruption bugs in our Rust-based compiler is that it’s a compiler.
Compilers emit machine instructions. When a machine executes those instructions, they can cause memory corruption, resulting in memory corruption bug reports from the people who experienced them. Regardless of which process had the bug—the compiler or compiled program—in both cases the processor only did the bad thing because the compiler told it to. And in both cases the fix is the same: the compiler’s code must change, since that code was what caused the memory corruption.
Just like every compiler, Roc’s has had bugs, and some of those have been miscompilations that led to memory corruption. That said, while 8 of the 10 memory corruption bugs in the Zig-based compiler were also miscompilations, the remaining 2 were in the compiler itself. Both were use-after-free bugs in error reporting, with the same symptom: filenames in error messages (one in roc check and the other in roc bundle) rendered as useless question-mark-in-diamond characters. Rust’s borrow checker would have caught both.
Now let’s suppose we had instead chosen Rust for our rewrite, or Zig with ReleaseSafe. What would have been the impact in practice, holding all else equal?
After 18 months of development, hundreds of total bug reports, and hundreds of thousands of lines of code, my main takeaway from retrospecting on this table is that picking a different row would have made no appreciable difference to the project. So far our choice has gotten us the outcome we’d hoped for.
As I noted earlier, every project has different needs. When Bun rewrote in the opposite direction—from Zig to Rust—their accompanying post noted:
For Bun, correctly handling the lifetimes of garbage-collected values [from JavaScript] and manually-managed values has been a major source of stability issues - most often small memory leaks and occasionally, crashes. Every memory allocation has to be meticulously reviewed. Where do these bytes get freed? How do we ensure it only gets freed once? Did we check for JavaScript exceptions properly? Is this garbage-collected pointer visible to the conservative stack scanner? Is this garbage collected memory or manually managed memory?
For Bun, correctly handling the lifetimes of garbage-collected values [from JavaScript] and manually-managed values has been a major source of stability issues - most often small memory leaks and occasionally, crashes. Every memory allocation has to be meticulously reviewed. Where do these bytes get freed? How do we ensure it only gets freed once? Did we check for JavaScript exceptions properly? Is this garbage-collected pointer visible to the conservative stack scanner? Is this garbage collected memory or manually managed memory?
Roc’s compiler doesn’t have these particular challenges because it doesn’t interface with JavaScript or any other tracing garbage collector. For Bun, “use-after-free, double-free, and ‘forgot to free’” errors have been “a large percentage of bugs,” whereas errors like these have been a small percentage of Roc’s bugs. And of course Roc’s compiler faces other challenges that Bun doesn’t. Different projects have different needs!
In our case, I’m not sure how I could look back at what’s actually happened and conclude that what we needed was a bigger investment in tooling to prevent memory safety bugs in the compiler itself. There’s a much stronger case that we would benefit from better tooling to catch memory safety bugs in our compiled output, which has always been out of scope for the borrow checker.
Build Times
We wanted faster builds from Zig. Did we get them?
Well, the good news is that zig build –watch -fincremental can rebuild a change to our current ~450K lines of Zig code in about 35 milliseconds. That’s even faster than what we were hoping for when we considered Zig’s build speed a selling point for the rewrite!
The bad news is that Zig’s current stable 0.16.0 release has a bug that breaks -fincremental on our code base. The fix already landed, but to get it we’d have to build on a nightly 0.17.0 prerelease build (which has breaking language changes), along with vendoring and upgrading our affected dependencies to 0.17.0. We decided to wait for the next stable release instead.
As of the last commit that had Rust sources in our code base, here’s a timing comparison on my Intel desktop machine running Ubuntu 26 for building cold (no cache, but packages downloaded locally) compared to doing an incremental rebuild after making a trivial edit to our parser:
Note that our Zig build configuration as of the feature-parity commit was rebuilding rarely-changing artifacts on every build that we later decided to rebuild only on demand. That’s why today’s cold builds are faster than they were back at 300K LoC, even though our lines of code have increased by ~50% since then.
Note that our Zig build configuration as of the feature-parity commit was rebuilding rarely-changing artifacts on every build that we later decided to rebuild only on demand. That’s why today’s cold builds are faster than they were back at 300K LoC, even though our lines of code have increased by ~50% since then.
Rust 1.97 is the current stable release today, and 1.85 was the current stable release 487 days ago (the time our rewrite took to reach to feature parity). So if we’d stayed on Rust for the same duration, we could have seen our incremental build times decrease from 10 seconds to 3.4. That’s a big jump! I really appreciate all the hard work that Rust contributors have done to improve build times. Eliminating 2/3 of our incremental build times over 18 months would have been a very welcome change if we’d stayed on Rust, and it’s a bigger improvement than I would have anticipated in an 18-month period. Bravo!
As impressive as that improvement is, Zig’s 35ms is still way ahead. Not only is it 1/100th the build time of 3.4 seconds, it’s also in a different performance category—and that 35ms is on a Zig code base with ~50% more lines of code than the Rust one that got 3.4s. I expect Roc’s code base to keep growing, and for this gap to keep growing with it; I’ve never heard of any initiative on Rust’s roadmap comparable to -fincremental.
So while our decision to remain on stable 0.16.0 (plus how many of our contributors run Mac laptops with ARM processors; -fincremental only works on x86 – 64 CPUs right now) means we haven’t yet reaped the anticipated build-time rewards of choosing Zig for the rewrite, we certainly have something to look forward to in the next stable Zig release!
Memory Control: Zero-Parse Deserialization
Roc’s new on-disk caching system uses a technique I first learned about from Zig’s compiler, and which Casey Muratori told me is common practice in game programming. It relies on the happy coincidence that if you’re organizing your memory in the way that runs fastest on modern hardware anyway, you can also load it from disk directly into memory and start using it without parsing anything.
Here’s how it works:
All of our compiler data structures are represented as arrays with 32-bit indices over pointers (and often in structure-of-arrays form).
This not only saves memory and runs faster, it also means our data structures can be written directly to disk without needing to be serialized into a different format first.
The bigger benefit is that this lets us deserialize them back into memory without parsing the on-disk bytes in any way. We load the bytes into memory, do some relocations to point our existing data structures to the newly-loaded arrays, and we’re ready to go.
This means we deserialize at the speed of loading the bytes from disk into memory—so, actually I/O bound. If those bytes are already in the operating system’s disk cache, it means we load cached work from previous builds at roughly the speed of memcpy.
When you run roc check twice in a row, the first time it caches all of its outputs on disk using this strategy. The second time, if the input source code files haven’t changed, all the parsed/type-checked/etc. data structures jump straight from disk into memory. It’s extremely fast. roc test similarly caches the outcomes for tests of pure functions (which are deterministic), and all of this is done with file-level granularity, so if you change one file you’ll only be paying for redoing work of that file and any others that depend on it.
This zero-parse deserialization strategy only works because we’re following this programming without pointers style for all of our compiler data structures. If we instead used pointers everywhere (like almost all compilers do), deserialization couldn’t be zero-parse.
This approach has safety risks, however. Similarly to how a pointer in memory can point to the wrong address (e.g. leading to a use-after-free), any index can be used as a lookup into the wrong array at runtime, at which point you end up with whatever random bytes happened to be at that location. Rust’s borrow checker is designed to help with pointer lifetimes, but it doesn’t attempt to answer the question “which index goes with which array?” because that has never been in scope for its design.
Date: 2026 – 07-15 Git: https://gitlab.com/mort96/blog/blob/published/content/00000-home/00017-sqlite-editions.md
SQLite is an amazing database engine. I use it as a database for plenty of embedded projects, and I don’t think it’s an exaggeration to call it the industry standard for local data storage. Some server software even uses it; for example, lobste.rs is now running on SQLite.
Unlike traditional RDBMSes (Relational DataBase Management Systems), SQLite is not a separate process; it’s an RDBMS as a library, meaning your software remains self contained. Unlike traditional file formats, you don’t need to write custom serializers and parsers. In some ways, it’s the best of both worlds.
There’s just one huge problem though. Its defaults are all wrong.
Bad default #1: Foreign key constraints are ignored by default
You read that right. Foreign key constraints are arguably the primary tool we have to ensure that a database remains consistent and don’t have dangling references.
As a quick primer, this is how an SQL foreign key constraint looks:
CREATE TABLE users ( id INTEGER PRIMARY KEY, display_name TEXT );
CREATE TABLE posts ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, content TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) );
The typical behavior for all other RDBMSes would be that the user_id column of a post must always reference the ID of a valid user. You can’t create a new post without providing a valid user ID, you can’t delete a user without also deleting its posts, lest you get a foreign key constraint violation error.
The only RDBMS I’m aware of which doesn’t enforce this by default is SQLite.
This is made even worse by SQLite’s tendency to re-use ROWID. You see, in this example, those INTEGER PRIMARY KEY rows become aliases for the table’s ROWID, which is a unique integer ID assigned to every row of a table in SQLite. The algorithm for assigning ROWID is a bit complicated (more details in the SQLite documentation), but it results in ID re-use in some cases. This means that a dangling reference easily results in a reference to the wrong row, which is even worse than a dangling reference because everything will seem fine. You don’t even get an error during lookup.
Just look at this hypothetical sequence of operations in our toy database schema:
– Bob creates a user account INSERT INTO users (display_name) VALUES (‘Bob’); SELECT * FROM users; — id | display_name — 1 | Bob
– Bob posts an introduction post INSERT INTO posts (user_id, content) VALUES (1, ‘Hello, I am Bob’); SELECT u.display_name, p.content FROM users as u, posts as p WHERE u.id = p.user_id; — display_name | content — Bob | Hello, I am Bob
– Bob deletes his account, — but we forgot to delete the posts. — SQLite doesn’t produce an error because it ignores our foreign key. DELETE FROM users WHERE id = 1;
– Alice creates an account. — Alice gets the same ID that Bob had due to the ROWID algorithm. INSERT INTO users (display_name) VALUES (‘Alice’); SELECT * FROM users; — id | display_name — 1 | Alice
– Alice has now inherited Bob’s old post! SELECT u.display_name, p.content FROM users as u, posts as p WHERE u.id = p.user_id; — display_name | content — Alice | Hello, I am Bob
The fix is to enable foreign_keys with a pragma:
PRAGMA foreign_keys = ON;
If we had done this in the beginning, the buggy DELETE would have produced an error:
DELETE FROM users WHERE id = 1; — Runtime error: FOREIGN KEY constraint failed (19)
Bad default #2: Columns can store the wrong data type
SQLite has a simple type system: a value can be NULL, an INTEGER, a REAL (aka a double precision float), TEXT, or a BLOB (aka binary data). Consequently, a column can be defined to hold values of any of those types.
However, a column defined as an INTEGER column isn’t restricted to only integers; instead, SQLite considers it to “use INTEGER affinity”. What this means is essentially:
If you try to insert a TEXT value, and it is a valid string representation of an integer, it is converted to an integer and stored as such.
If you try to insert a TEXT value, and it is a valid string representation of a real number, it is converted to a real (aka double precision float) and stored as such.
Otherwise, the value is stored as-is.
Other affinities have different but simpler rules:
Columns with BLOB affinity store values as-is.
Columns with TEXT affinity store BLOB, TEXT and NULL values as-is, but convert numeric values to TEXT.
Columns with REAL affinity work like columns with INTEGER affinity except that integer values are converted to REAL.
Here’s how this looks in practice:
CREATE TABLE music ( id INTEGER PRIMARY KEY, name TEXT, duration_sec INTEGER );
INSERT INTO music (name, duration_sec) VALUES (‘Lost In Hollywood’, 321); INSERT INTO music (name, duration_sec) VALUES (‘Comfortably Numb’, 382); INSERT INTO music (name, duration_sec) VALUES (‘The Way of All Flesh’, ‘Way too long, I mean come on’); SELECT * FROM music; — id | name | duration_sec — 1 | Lost In Hollywood | 321 — 2 | Comfortably Numb | 382 — 3 | The Way of All Flesh | Way too long, I mean come on
I don’t think I need to explain why it’s a bad idea for a database to be so careless about data validation. It would be one thing if SQLite was an explicitly dynamically typed document database, but it’s not. SQLite asks me through its syntax rules, “What type do you want to go into this column”.
I once had to clean up a project where some code had accidentally been writing the strings ‘1’ and ‘0’ to a column which was intended to store booleans (1 and 0). That was not a fun debugging story.
Luckily, SQLite has the concept of strict tables, which makes SQLite produce a type error when the wrong type is inserted into a column:
CREATE TABLE music ( id INTEGER PRIMARY KEY, name TEXT, duration_sec INTEGER ) strict;
INSERT INTO music (name, duration_sec) VALUES (‘The Way of All Flesh’, ‘Way too long, I mean come on’); — Runtime error: cannot store TEXT value in INTEGER column music.duration_sec (19)
Unfortunately, there is no pragma to globally make all tables strict. So you have to remember to add the strict tag to every table manually.
There’s a couple of arguments against strict tables which I want to cover here.
The authors of SQLite have written about their preference for “flexible typing”. Personally, I find this a really strange piece of writing. It doesn’t provide any examples for why it could ever be useful to insert a BLOB into an INTEGER column. All it does is illustrate why it’s sometimes useful to have a column which can store values of any type. Strict tables have a solution for that; it’s called the ANY data type. You can still create columns which accept any value, you just have to be explicit about it.
A much better argument is provided by user ‘zie’ on lobste.rs. You see, strict tables in SQLite don’t just enforce types. They also change the rules for how type specifiers are parsed.
Non-strict SQLite tables use the following rules to determine the type of a column (from SQLite’s documentation):
If the declared type contains the string “INT” then it is assigned INTEGER affinity. If the declared type of the column contains any of the strings “CHAR”, “CLOB”, or “TEXT” then that column has TEXT affinity. Notice that the type VARCHAR contains the string “CHAR” and is thus assigned TEXT affinity. If the declared type for a column contains the string “BLOB” or if no type is specified then the column has affinity BLOB. If the declared type for a column contains any of the strings “REAL”, “FLOA”, or “DOUB” then the column has REAL affinity. Otherwise, the affinity is NUMERIC.
If the declared type contains the string “INT” then it is assigned INTEGER affinity.
If the declared type of the column contains any of the strings “CHAR”, “CLOB”, or “TEXT” then that column has TEXT affinity. Notice that the type VARCHAR contains the string “CHAR” and is thus assigned TEXT affinity.
If the declared type for a column contains the string “BLOB” or if no type is specified then the column has affinity BLOB.
If the declared type for a column contains any of the strings “REAL”, “FLOA”, or “DOUB” then the column has REAL affinity.
Otherwise, the affinity is NUMERIC.
A consequence of this rule, combined with SQLite’s loose typing, is that you can give your columns type names such as DATETIME or KEY_VALUE_SET or COLOR, and have a database connector/wrapper which automatically knows to serialize and deserialize columns with custom types. And if nothing else, those custom type names serve as useful documentation.
I have to acknowledge that just changing the default from non-strict tables to strict tables, with no further changes, would give up on this somewhat nifty quirk. However, I think we would be much better served by custom type aliases.
If we could write something like:
CREATE TYPE KEY_VALUE_SET = TEXT;
and then use KEY_VALUE_SET as a type name in a strict table, I think everyone would be happy. I would probably start using such a feature liberally to document the expected pattern of data in my columns. In a real world schema, you inevitably end up with TEXT columns which have to be parsed by application code.
As an aside to this aside, it would be neat if we could associate CHECK constraints with a custom type.
Update: ‘masklinn’ on lobste.rs points out that the SQL 99 standard already specifies type aliases, called CREATE DOMAIN. This already supports constraints as well. So really, SQLite just needs to add support for the standard CREATE DOMAIN statement.
Update: ‘masklinn’ on lobste.rs points out that the SQL 99 standard already specifies type aliases, called CREATE DOMAIN. This already supports constraints as well. So really, SQLite just needs to add support for the standard CREATE DOMAIN statement.
Bad default #3: SQLITE_BUSY errors with concurrent writers
SQLite allows multiple concurrent readers, but only one writer at a time. By default, if you have two processes trying to acquire a write lock at the same time, one of them will immediately receive an SQLITE_BUSY error.
This is not the behavior I expect. I expect SQLite to wait for the lock to get unlocked, up to some timeout. It’s doing disk IO after all, so I already structure my code with the assumption that a write could potentially be slow.
The default behavior has lead me to writing real-world bugs, where systems would sometimes just crash. I’ve manually written retry loops to fix it.
The fix is to set busy_timeout with a pragma:
PRAGMA busy_timeout = 5000;
This makes SQLite try to acquire the lock for up to 5 seconds before erroring with a SQLITE_BUSY error.
I didn’t learn about this setting until recently. It seems like such an obvious default that I’m astonished that it’s not.
Update: I should add a note here about why support for concurrent writers is desirable. During normal operation, you’re usually best served by structuring your software such that all writes are done by a single process, ideally a single thread. Concurrent writers will never be fast. But there are non-typical situations. Maybe you need to manually clean up a database interactively using the sqlite3 tool interactively on the command line. Maybe you have scripts for uncommon administrative tasks which you haven’t had the need to write a front-end for. These are perfectly legitimate and, I believe, fairly common use cases. I think it’s bad that with SQLite’s defaults, this kind use has a chance to just crash the software by making it throw an unexpected SQLITE_BUSY error.
Update: I should add a note here about why support for concurrent writers is desirable.
During normal operation, you’re usually best served by structuring your software such that all writes are done by a single process, ideally a single thread. Concurrent writers will never be fast. But there are non-typical situations. Maybe you need to manually clean up a database interactively using the sqlite3 tool interactively on the command line. Maybe you have scripts for uncommon administrative tasks which you haven’t had the need to write a front-end for. These are perfectly legitimate and, I believe, fairly common use cases. I think it’s bad that with SQLite’s defaults, this kind use has a chance to just crash the software by making it throw an unexpected SQLITE_BUSY error.
Bad default 4: Performance
There’s a lot to say about performance tuning in SQLite. When correctly configured, it can be a truly fast RDBMS, with the ability to fill roles we typically reserve for the big servers like PostgreSQL or MySQL.
But by default, its performance isn’t great. Smarter people than me have written much more on this, and I recommend Sylvain Kerkour’s Optimizing SQLite for servers if you’re interested in this topic.
But the most significant bad default is that SQLite’s Write-Ahead Log (WAL) is disabled by default. It can be enabled with:
PRAGMA journal_mode = WAL;
The WAL provides a dramatic write speed-up in most circumstances. Additionally, it lets us drastically reduce the amount of disk syncs without risking data corruption by changing another setting:
PRAGMA synchronous = NORMAL;
See the SQLite documentation on what exactly synchronous does.
The solution: editions?
The oft-cited reason for why these defaults remain, well, default, is of course backwards compatibility. Changing defaults now would likely break lots of old software and make people afraid to upgrade SQLite in the future in case it breaks everything again, just like how I’m afraid to upgrade Python because every “upgrade” breaks a bunch of software I use. It’s a laudible and rare goal to try to not break your dependents.
However, I think the solution is simple: add one “super pragma” which changes all the bad defaults. I propose that the following:
PRAGMA edition = 2026;
should be an alias for at least the following set of pragmas:
PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;
And also make strict mode the default for tables.
This should be a nice middle ground which avoids breaking backwards compatibility, but lets the database engine move forwards and not be bogged down by its own history.
The edition idea is stolen straight from Rust editions. The advantage of a year-based edition rather than something like JavaScript’s “use strict”; is that as the years go by, the sensible defaults may change. Maybe something like Hctree’s WAL2 makes its way into the main branch, say, in the year 2034, so maybe PRAGMA edition = 2034 will some day set PRAGMA journal_mode = WAL2.
Anyway, that’s all. I think SQLite should have an edition system with updated sets of defaults. Are there any things I’ve missed which makes this a bad idea? Or more pragmas which should be added to my imaginary “2026 edition”?
I like my new-to-me MacBook because it enables me to create more stuff compared to my now almost 8 year old Thinkpad I had before, mainly due to battery and screen reasons. There is one thing about it that I cannot wrap my head around from a design perspective and it’s the sharp edges especially around the wrist area. On a flat surface it’s no problem at all, but low and behold a laptop will be frequently used on a lap, meaning the wrists will touch the sharp edge at an angle which is very uncomfortable.
I’ve found some posts online about people that have filed the edges off of their Macs before, like this nice one by Kent Walters. In my case however, almost no one freaks out about this because no one actually notices it. It went well, so I thought why not document the process.
Note I am absolutely not a good craftsman, so do not use this as a guide!
I thought a long time about how to do it so as not to have it result in a wavy bezel. Although I really wanted to use this project as an excuse to buy a random orbital sander, I very quickly pivoted from this idea because I would probably cause more damage with it than I would with a hand file. Another idea was to 3D print some sort of chamfer to use as a guide, just to have the initial part cleanly taken off at an angle. I played around with this idea for a while but concluded that I could probably not get to the required level of precision I wanted, especially since it would have to hold the file/sandpaper. I ended up just using general purpose metal file I had at hand and progressive sand paper (in block form, strongly recommended).
Some tape helped me to mark the areas from below and above the bezel that I wanted to have file off. This worked really well. Of course I taped off the trackpad, the keyboard and headphone plug etc. to avoid any residue from getting inside. Additionally, I used very little soapy water that I applied to the sandpaper throughout to keep the dust contained somewhat. The initial filing with the general metal file was a bit scary, but it did not take off too much and the tape helped me to achieve an even level. The sandpaper blocks also helped with that, and I went up to 1200 coarseness.
The tricky part for me were these pointy dots on the little gap in the middle. I did not want to go too heavy here with the filing so I used some model making files very, very lightly and then just used the 1200 sandpaper until I was satisfied with the result. In the end, I was careful to remove any dust that had accumulate, for which this little air blower squeegy thing came in handy.
This is a “blue” M4 MacBook Air, so I’m curious how it will look over time due to the anodized aluminum. Another note would be to test it out a bit and see how it feels. I have quite large arms so when typing I also touch the corners, so I had to go back and file them too, but that might not be necessary depending on how you do it.
What I liked about the Thinkpad was that I could just chuck it anywhere without worrying about it. I made it a point when I bought this Mac to treat it just the same way. Ultimately, it is a tool and should be used appropriately. Even if it is nice and shiny, if modifying it would make it serve it’s primary purpose as a tool better, it’s worth a consideration. So I like this recent wave of encouragement around similar modifications, and it is also far easier and approachable than it seems initially.
Counting each YC chapter by batch year shows when these founders came through. A founder with two YC startups appears in both years.
To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".
10HN is also available as an iOS App
If you visit 10HN only rarely, check out the the best articles from the past week.
Visit pancik.com for more.