Static Allocation with Zig
Key topics
The debate around static allocation with Zig has sparked a lively discussion, with some commenters defending it as a standard practice in embedded programming, while others question its application in a database context. As it turns out, static allocation is not a novel concept, with uses in various domains such as video games, kernels, and crypto libraries, as pointed out by diath. The original author's decision to use static allocation in a database has raised eyebrows, with some seeing it as an interesting choice, while others view it as a documented style guide rather than a groundbreaking technique, as noted by codys. Amidst the discussion, some commenters called out snide remarks, advocating for a more constructive tone, as highlighted by mitchellh.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
11m
Peak period
77
0-6h
Avg / period
13.9
Based on 111 loaded comments
Key moments
- 01Story posted
Dec 29, 2025 at 11:07 AM EST
12 days ago
Step 01 - 02First comment
Dec 29, 2025 at 11:18 AM EST
11m after posting
Step 02 - 03Peak activity
77 comments in 0-6h
Hottest window of the conversation
Step 03 - 04Latest activity
Dec 31, 2025 at 8:27 PM EST
9 days ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
It's baffling that a technique known for 30+ years in the industry have been repackage into "tiger style" or whatever this guru-esque thing this is.
Virgil II and III inherited this. It's a standard part of a Virgil program that its components and top-level initializers run at compile time, and the resulting heap is then optimized and serialized into the binary. It doesn't require passing allocators around, it's just part of how the language works.
It’s tempting to cut people down to size, but I don’t think it’s warranted here. I think TigerBeetle have created something remarkable and their approach to programming is how they’ve created it.
...and video games, and kernels, and crypto libraries, and DSPs, and... you can go on. It's not a novel concept. It's just the Silicon Valley buffoons giving an existing concept a fancy name to look more appealing to clueless investors.
Allocation aside, many optimizations require knowing precisely how close to instantaneous resource limits the software actually is, so it is good practice for performance engineering generally.
Hardly anyone does it (look at most open source implementations) so promoting it can’t hurt.
NB: I was just a newbie back then, so any older grey beards, please feel free to correct me. But I distinctly remember supporting commercial databases as being one of the justifications for overcommit. (Despite overcommit not being typical in the environments originally running those DBs, AFAIU.)
[1] Note that AFAIU the BSDs had overcommit, too, but just for fork + CoW. (These days FreeBSD at least has overcommit more similar to Linux.) Solaris actually does strict accounting even for fork, and I assume that was true back in the 90s. Did any commercial Unices do overcommit by default?
I think the more constructive reality is discussing why techniques that are common in some industries such as gaming or embedded systems have had difficulty being adopted more broadly, and celebrating that this idea which is good in many contexts is now spreading more broadly! Or, sharing some others that other industries might be missing out on (and again, asking critically why they aren't present).
Ideas in general require marketing to spread, that's literally what marketing is in the positive (in the negative its all sorts of slime!). If a coding standard used by a company is the marketing this idea needs to live and grow, then hell yeah, "tiger style" it is! Such is humanity.
Most applications don’t need to bother the user with things like how much memory they think will be needed upfront. They just allocate how much and when necessary. Most applications today are probably servers that change all the time. You would not know upfront how much memory you’d need as that would keep changing on every release! Static allocation may work in a few domains but it certainly doesn’t work in most.
What this article is talking about isn't all the way at the other end (compile time allocation), but has the additional freedom that you can decide allocation size based on runtime parameters. That frees the rest of the application from needing to worry about managing memory allocations.
We can imagine taking another step and only allocating at the start of a connection/request, so the rest of the server code doesn't need to deal with managing memory everywhere. This is more popularly known as region allocation. If you've ever worked with Apache or Nginx, this is what they do ("pools").
So on and so forth down into the leaf functions of your application. Your allocator is already doing this internally to help you out, but it doesn't have any knowledge of what your code looks like to optimize its patterns. Your application's performance (and maintainability) will usually benefit from doing it yourself, as much as you reasonably can.
Not even calculus, just basic arithmetic operations.
I dont think we need marketing, but rather education, which is the actually useful way to spread information.
If you think marketing is the way knowledge spreads, you'll end up with millions of dollars in your pocket and the belief that you have money because you're doing good, while the truth is that you have millions because you exploited others.
"One of those techniques is static memory allocation during initialization. The idea here is that all memory is requested and allocated from the OS at startup, and held until termination. I first heard about this while learning about TigerBeetle, and they reference it explicitly in their development style guide dubbed "TigerStyle"."
Anyways, TigerStyle is inspired by NASA's Power of Ten whitepaper on Rules For Developing Safety Critical Code:
https://github.com/tigerbeetle/tigerbeetle/blob/ac75926f8868...
You might be impressed by that fact or the original Power of Ten paper but if so, it's only because NASA's marketing taught you to be.
Incidentally, I was aware of NASA paper before tigerbeetle was a thing, nit because someone marketed their work, but because I did my research over published ones.
That's why AI-sloppy software would go viral and make loads of money while properly engineered ones die off.
When people need knowledge, they know where to find it. They don't need marketing for that.
Knowledge sharing with next generations is one of those very tricky things.
For one thing, how would I know where to find this? What book? What teacher? There are so many books, must I read all of them? What if my coworkers awaren't aware of it, how can they share it with me?
Also, an old saying goes, if you're good at something, never do it for free. This isn't exactly a trade secret, but how many people blog about every advanced technique and trick they know? I blogged about how to create real C function pointers from Lua closures, as a way to advertise for my product, but that could very well have been kept a trade secret (and probably should have, as I got 0 sales from that blog post still). Why would anyone want to share this "tiger style" knowledge with newer generations with no personal benefit? Aren't they incentivized to use it secretly, or maybe write it in a book, or blog about it for advertising?
I think it's when people consider that anything more than 5 years old is ancient and dismiss it that we lose established techniques and knowledge that we are then bound to rediscover again and again.
An ability to pick topics, not as widely known as they are useful, develop a well received voice/style, validated by increasing visibility, is where to start.
Only when you know ahead of time that a unique post has readership would it be worth sharing. If the goal is external (i.e. a marketing/sales bump), as apposed to internal (just enjoy sharing, or using writing to self-clarify).
> NASA's Power of Ten — Rules for Developing Safety Critical Code will change the way you code forever. To expand:
* https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TI...
* https://spinroot.com/gerard/pdf/P10.pdf
"For fairly pragmatic reasons, then, our coding rules primarily target C and attempt to optimize our ability to more thoroughly check the reliability of critical applications written in C."
A version of this document targeting, say, Ada would look quite different.
The ESA Ada standard also recommends all allocation occur at initialization, and requires exceptions to be justified.
The rules are written with the historical context of C making it too easy to leak heap-allocated memory. In the safety-critical Rust code that I've worked on, we tend not to dynamically allocate due to the usual constraints, and we're well aware of the "thou shalt not allocate" rules in the scripture, but we've already gotten clearance from the relevant certification authorities that Rust is exempt from the restriction against dynamic allocation specifically because of its ownership system.
This is also how GPU shader programming works: there's no real equivalent to heap allocation or general pointers, you're expected to work as far as possible with local memory. So the technique may be quite relevant in the present day, even though it has a rather extensive history of its own.
On the surface it seems great: infinite scale and perfectly generic. The system can handle anything. But does it need to handle everything? And, what's the cost in terms of complexity of handling every single possible scenario?
The fact that the Zig ecosystem follows the pattern set by the standard library to pass the Allocator interface around makes it super easy to write idiomatic code, and then decide on your allocation strategies at your call site. People have of course been doing this for decades in other languages, but it's not trivial to leverage existing ecosystems like libc while following this pattern, and your callees usually need to know something about the allocation strategy being used (even if only to avoid standard functions that do not follow that allocation strategy).
It’s the only kind of program that can be actually reasoned about. Also, not exactly Turing complete in classic sense.
Makes my little finitist heart get warm and fuzzy.
It’s actually quite tricky though. The allocation still happens and it’s not limited to, so you could plausibly argue both ways.
That "once all the input has been given to the program" is doing a bit of heavy lifting since we have a number of programs where we have either unbounded input, or input modulated by the output itself (e.g., when a human plays a game their inputs are affected by previous outputs, which is the point after all), or other such things. But you can model all programs as their initial contents and all inputs they will ever receive, in principle if not in fact, and then your program is really just a finite state automaton.
Static allocation helps make it more clear, but technically all computers are bounded by their resources anyhow, so it really doesn't change anything. No program is Turing complete.
The reason why we don't think of them this way is several fold, but probably the most important is that the toolkit you get with finite state automata don't apply well in our real universe to real programs. The fact that mathematically, all programs can in fact be proved to halt or not in finite time by simply running them until they either halt or a full state of the system is repeated is not particularly relevant to beings like us who lack access to the requisite exponential space and time resources necessary to run that algorithm for real. The tools that come from modeling our systems as Turing Complete are much more practically relevant to our lives. There's also the fact that if your program never runs out of RAM, never reaches for more memory and gets told "no", it is indistinguishable from running on a system that has infinite RAM.
Technically, nothing in this universe is Turing Complete. We have an informal habit of referring to things that "would be Turing Complete if extended in some reasonably obvious manner to be infinitely large" as simply being Turing Complete even though they aren't. If you really, really push that definition, the "reasonably obvious manner" can spark disagreements, but generally all those disagreements involve things so exponentially large as to be practically irrelevant anyhow and just be philosophy in the end. For example, you can't just load a modern CPU up with more and more RAM, eventually you would get to the point where there simply isn't enough state in the CPU to address more RAM, not even if you hook together all the registers in the entire CPU and all of its cache and everything else it has... but such an amount of RAM is so inconceivably larger than our universe that it isn't going to mean anything practical in this universe. You then get into non-"obvious" ways you might extend it from there, like indirect referencing through other arbitrarily large values in RAM, but it is already well past the point where it has any real-world meaning.
Also it's giving me flashbacks to LwIP, which was a nightmare to debug when it would exhaust its preallocated buffer structures.
We used to have very little memory, so we developed many tricks to handle it.
Now we have all the memory we need, but tricks remained. They are now more harmful than helpful.
Interestingly, embedded programming has a reputation for stability and AFAIK game development is also more and more about avoiding dynamic allocation.
Under these conditions, you do need a fair bit of dynamism, but the deallocations can generally be in big batches rather than piecemeal, so it's a good fit for slab-type systems.
Also, is easier to refactor if you do the typical GC allocation patterns. Because you have 1 million different lifetimes and nobody actually knows them, except the GC kind of, it doesn't matter if you dramatically move stuff around. That has pros and cons, I think. It makes it very unclear who is actually using what and why, but it does mean you can change code quickly.
That might have been the case ~30 years ago on platforms like the Gameboy (PC games were already starting to use C++ and higher level frameworks) but certainly not today. Pretty much all modern game engines allocate and deallocate stuff all the time. UE5's core design with its UObject system relies on allocations pretty much everywhere (and even in cases where you do not have to use it, the existing APIs still force allocations anyway) and of course Unity using C# as a gameplay language means you get allocations all over the place too.
Aka you minimize allocations in gameplay.
LwIPs buffers get passed around across interrupt handler boundaries in and out of various queues. That's that makes it hard to reason about. The allocation strategy is still sound when you can't risk using a heap.
Theoretically infinite memory isn't really the problem with reasoning about Turing-complete programs. In practice, the inability to guarantee that any program will halt still applies to any system with enough memory to do anything more than serve as an interesting toy.
I mean, I think this should be self-evident: our computers already do have finite memory. Giving a program slightly less memory to work with doesn't really change anything; you're still probably giving that statically-allocated program more memory than entire machines had in the 80s, and it's not like the limitations of computers in the 80s made us any better at reasoning about programs in general.
Static allocation requires you to explicitly handle overflows, but also by centralizing them, you probably need not to have as many handlers.
Technically, all of this can happen as well in language with allocations. It’s just that you can’t force the behavior.
Most programs have logical splits where you can allocate. A spreadsheet might allocate every page when it's created, or a browser every tab. Or a game every level. We can even go a level deeper if we want. Maybe we allocate every sheet in a spreadsheet, but in 128x128 cell chunks. Like Minecraft.
No. That is one restriction that allows you to theoretically escape the halting problem, but not the only one. Total functional programming languages for example do it by restricting recursion to a weaker form.
Also, more generally, we can reason about plenty of programs written in entirely Turing complete languages/styles. People keep mistaking the halting problem as saying that we can never successfully do termination analysis on any program. We can, on many practical programs, including ones that do dynamic allocations.
Conversely, there are programs that use only a statically bounded amount of memory for which this analysis is entirely out of reach. For example, you can write one that checks the Collatz conjecture for the first 2^1000 integers that only needs about a page of memory.
What do you mean? There are loads of formal reasoning tools that use dynamic allocation, e.g. Lean.
But why? If you do that you are just taking memory away from other processes. Is there any significant speed improvement over just dynamic allocation?
2. Speed improvement? No. The improvement is in your ability to reason about memory usage, and about time usage. Dynamic allocations add a very much non-deterministic amount of time to whatever you're doing.
But if you're assuming that overcommit is what will save you from wasting memory in this way, then that sabotages the whole idea of using this scheme in order to avoid potential allocation errors.
- Operational predictability --- latencies stay put, the risk of threshing is reduced (_other_ applications on the box can still misbehave, but you are probably using a dedicated box for a key database)
- Forcing function to avoid use-after-free. Zig doesn't have a borrow checker, so you need something else in its place. Static allocation is a large part of TigerBeetle's something else.
- Forcing function to ensure existence of application-level limits. This is tricky to explain, but static allocation is a _consequence_ of everything else being limited. And having everything limited helps ensure smooth operations when the load approaches deployment limit.
- Code simplification. Surprisingly, static allocation is just easier than dynamic. It has the same "anti-soup-of-pointers" property as Rust's borrow checker.
Doesn't reusing memory effectively allow for use-after-free, only at the progam level (even with a borrow checker)?
I would say the main effect here is that global allocator often leads to ad-hoc, "shotgun" resource management all other the place, and that's hard to get right in a manually memory managed language. Most Zig code that deals with allocators has resource management bugs (including TigerBeetle's own code at times! Shoutout to https://github.com/radarroark/xit as the only code base I've seen so far where finding such bug wasn't trivial). E.g., in OP, memory is leaked on allocation failures.
But if you manage resources manually, you just can't do that, you are forced to centralize the codepaths that deal with resource acquisition and release, and that drastically reduces the amount of bug prone code. You _could_ apply the same philosophy to allocating code, but static allocation _forces_ you to do that.
The secondary effect is that you tend to just more explicitly think about resources, and more proactively assert application-level invariants. A good example here would be compaction code, which juggles a bunch of blocks, and each block's lifetime is tracked both externally:
* https://github.com/tigerbeetle/tigerbeetle/blob/0baa07d3bee7...
and internally:
* https://github.com/tigerbeetle/tigerbeetle/blob/0baa07d3bee7...
with a bunch of assertions all other the place to triple check that each block is accounted for and is where it is expected to be
https://github.com/tigerbeetle/tigerbeetle/blob/0baa07d3bee7...
I see a weak connection with proofs here. When you are coding with static resources, you generally have to make informal "proofs" that you actually have the resource you are planning to use, and these proofs are materialized as a web of interlocking asserts, and the web works only when it is correct in whole. With global allocation, you can always materialize fresh resources out of thin air, so nothing forces you to do such web-of-proofs.
To more explicitly set the context here: the fact that this works for TigerBeetle of course doesn't mean that this generalizes, _but_, given that we had a disproportionate amount of bugs in small amount of gpa-using code we have, makes me think that there's something more here than just TB's house style.
You mentioned, "E.g., in OP, memory is leaked on allocation failures." - Can you clarify a bit more about what you mean there?
A) clean up individual allocations on failure:
B) ask the caller to pass in an arena instead of gpa to do bulk cleanup (types & code stays the same, but naming & contract changes): C) declare OOMs to be fatal errors You might also be interesting in https://matklad.github.io/2025/12/23/static-allocation-compi..., it's essentially a complimentary article to what @MatthiasPortzel says here https://news.ycombinator.com/item?id=46423691The allocation failure that could occur at runtime, post-init, would be here: https://github.com/nickmonad/kv/blob/53e953da752c7f49221c9c4... - and the OOM error kicks back an immediate close on the connection to the client.
Notice that this kind of use-after-free is a ton more benign though. This milder version upholds type-safety and what happens can be reasoned about in terms of the semantics of the source language. Classic use-after-free is simply UB in the source language and leaves you with machine semantics, usually allowing attackers to reach arbitrary code execution in one way or another.
On the other hand, the more empirical, though qualitative, claim made by by matklad in the sibling comment may have something to it.
[1]: A program written in Assembly has no UB, and all of its behaviours can be reasoned about in the source language, but I'd hardly trust Assembly programs to be safer than C programs.
FWIW, I don't find this argument logically sound, in context. This is data aggregated across programming languages, so it could simultaneously be true that, conditioned on using memory unsafe language, you should worry mostly about UB, while, at the same time, UB doesn't matter much in the grand scheme of things, because hardly anyone is using memory-unsafe programming languages.
There were reports from Apple, Google, Microsoft and Mozilla about vulnerabilities in browsers/OS (so, C++ stuff), and I think there UB hovered at between 50% and 80% of all security issues?
And the present discussion does seem overall conditioned on using a manually-memory-managed language :0)
1. In the context of languages that can have OOB and/or UAF, OOB/UAF are very dangerous, but not necessarily because they're UB; they're dangerous because they cause memory corruption. I expect that OOB/UAF are just as dangerous in Assembly, even though they're not UB in Assembly. Conversely, other C/C++ UBs, like signed overflow, aren't nearly as dangerous.
2. Separately from that, I wanted to point out that there are plenty of super-dangerous weaknesses that aren't UB in any language. So some UBs are more dangerous than others and some are less dangerous than non-UB problems. You're right, though, that if more software were written with the possibility of OOB/UAF (whether they're UB or not in the particular language) they would be higher on the list, so the fact that other issues are higher now is not relevant to my point.
I'd put it like this:
Undefined behavior is a property of an abstract machine. When you write any high-level language with an optimizing compiler, you're writing code against that abstract machine.
The goal of an optimizing compiler for a high-level language is to be "semantics-preserving", such that whatever eventual assembly code that gets spit out at the end of the process guarantees certain behaviors about the runtime behavior of the program.
When you write high-level code that exhibits UB for a given abstract machine, what happens is that the compiler can no longer guarantee that the resulting assembly code is semantics-preserving.
1. Doesn't the overcommit feature lessen the benefits of this? Your initial allocation works but you can still run out of memory at runtime.
2. For a KV store, you'd still be at risk of application level use-after-free bugs since you need to keep track of what of your statically allocated memory is in use or not?
For the second question, yes, we have to keep track of what's in use. The keys and values are allocated via a memory pool that uses a free-list to keep track of what's available. When a request to add a key/value pair comes in, we first check if we have space (i.e. available buffers) in both the key pool and value pool. Once those are marked as "reserved", the free-list kind of forgets about them until the buffer is released back into the pool. Hopefully that helps!
https://github.com/kristoff-it/zig-okredis
As a tiny nit, TigerBeetle isn't _file system_ backed database, we intentionally limit ourselves to a single "file", and can work with a raw block device or partition, without file system involvement.
those features all go together as one thing. and it's the unix way of accessing block devices (and their interchangeability with streams from the client software perspective)
you're right, it's not the file system.
Memcached works similarly (slabs of fixed size), except they are not pre-allocated.
If you're sharing hardware with multiple services, e.g. web, database, cache, the kind of performance this is targeting isn't a priority.
Zig also works "okay" with vibe coding. Golang works much better (maybe a function of the models I use (primarily through Cursor) or maybe its that there's less Zig code out in the wild for models to scrape and learn from, or maybe idiomatic Zig just isn't a thing yet the way it is with Go. Not quite sure.
amount of examples on the web is not a good predictor of llm capability, since we know you can poison an llm with ~250 examples. it doesn't take much.
I think there are a few different ways to approach the answer, and it kind of depends on what you mean by "draw the line between an allocation happening or not happening." At the surface level, Zig makes this relatively easy, since you can grep for all instances of `std.mem.Allocator` and see where those allocations are occurring throughout the codebase. This only gets you so far though, because some of those Allocator instances could be backed by something like a FixedBufferAllocator, which uses already allocated memory either from the stack or the heap. So the usage of the Allocator instance at the interface level doesn't actually tell you "this is for sure allocating memory from the OS." You have to consider it in the larger context of the system.
And yes, we do still need to track vacant/occupied memory, we just do it at the application level. At that level, the OS sees it all as "occupied". For example, in kv, the connection buffer space is marked as vacant/occupied using a memory pool at runtime. But, that pool was allocated from the OS during initialization. As we use the pool we just have to do some very basic bookkeeping using a free-list. That determines if a new connection can actually be accepted or not.
Hopefully that helps. Ultimately, we do allocate, it just happens right away during initialization and that allocated space is reused throughout program execution. But, it doesn't have to be nearly as complicated as "reinventing garbage collection" as I've seen some other comments mention.
Why not then just re-define std.mem.Allocator to interact with your free list only and never talk to the operating system? I fail to see how this is even interesting.
> At that level, the OS sees it all as "occupied".
Right. But your program still allocates memory, but now from a free list. I think my confusion is about whether there's any difference from a high level point of view between allocating from a free-list and allocating from RAM pages. From a C programming point of view, you can just redefine malloc() to allocate to and from a free-list - but it would still basically only be a custom malloc().
I don't feel like that's the case as in many other languages you can declare all your objects in a generic package with your sizes as generic parameters and avoid allocation all together.