---
title: "Arena Allocators in C"
slug: arena-allocators-in-c
url: https://listedarticles.com/articles/arena-allocators-in-c
canonical_url: https://eliasebner.com/blog/guides/arena-allocators-in-c/
content_type: tutorial
language: en
published_at: 2026-10-11T00:00:00.000Z
updated_at: 2026-10-11T17:12:49.696Z
author: "Elias Ebner"
authored_by: human
publisher: "eliasebner.com"
publisher_url: https://eliasebner.com/
topics: ["Programming"]
license: all-rights-reserved
word_count: 2184
reading_minutes: 9
citation: "Elias Ebner, eliasebner.com. \"Arena Allocators in C.\" 11 Oct 2026. https://eliasebner.com/blog/guides/arena-allocators-in-c/ (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# Arena Allocators in C

> A practical introduction to arena allocators in C: what they are, why grouping allocations into one region makes memory management faster and less leak-prone, and a step-by-step personal implementation with code, plus the trade-offs of using them.

Oct 11, 2026

# Arena Allocators in C

---

As we all know, C is a very low level language.
It allows you (and kind of forces you) to manage the memory you use by yourself.
This is one of the (probably many) reasons that people are turn to other languages when
it comes to their daily programming.
In this article, I will show you a paradigm for managing memory which is not only very
performant, but it’s also extremely versatile. It makes the task significantly easier
and decrease the chance of memory leaks by a large margin.

Maybe you already know about them, maybe not, in any case: I’m talking about arena
allocators.

I will get to the implementation, but before I do that, here is a brief introduction
to the concept of an arena allocator.

## What Are Arena Allocators?

The way people usually go about managing memory is that they allocate
a small amount of memory every time they need it from the heap with `malloc` - just what they need.
If they find out later that they need more than what was originally planned, they use `realloc`
to increase the size of their memory allocation.

Once they are done with using that memory, they call `free` to tell the operating system that this
memory is no longer needed and it can be employed for other tasks.

This is great, but it has a few issues:

**1.**

The programmer must remember to free the memory once he is done using it. This is especially dangerous
if the allocation happens in a loop - like in games, for example - since in that case the operating system
keeps allocating more and more memory, but that memory is never freed.

Memory, as a resource, is not infinite. At some point, your computer will run out, and your program
will be terminated by the operating system (i.e. it will crash).

**2.**

Allocating and freeing many small chunks of memory is *slow*.

**3.**

If you have a function like:

```
i32 *create_array(usize len) {
    i32 *elements = (i32 *)malloc(sizeof(int));
    return elements;
}
```

Now, tell me: how does the programmer calling the function know that this
function performs a memory allocation? In this case, one could figure it out by
looking at the name of the function and thinking about what the function could
possibly be doing, but it’s not always so trivial.

Remember, this is important, since the programmer needs to know when to free the memory.

The programmer needs to be made aware of the fact that it is his responsibility to free
the function.

There are probably more issues that I have not thought about, but these are some of
the main ones I could think about on the spot.

**The Solution.**

Enter *the arena allocator*.

Most lifetimes (i.e. how long a certain region of memory is used before it is freed)
can be grouped into one of a handful of categories:

1. The memory lives for the duration of the entire program
2. The memory lives until the completion of some tasks (i.e. a frame in a game or the handling of an HTTP request)
3. The memory lives for the scope of a function

This is usually enough grouping, but depending on what you consider a group, there may be more.
In any case, we can just allocate a large chunk that we will use to work with things that have a similar
lifetime (i.e. things that can be freed together).

We then have a large amount of memory that can be used. Whenever we use some of that memory to do something,
we advance a pointer to remember up to which point the memory has been used and where the free memory begins.

This will become clearer once you see the implementation.

When we don’t need any of the data that lives inside that large chunk of memory anymore, we can simply
free it all at once (or - if we want to reuse that same memory - reinitialize the memory arena to
its initial state and begin allocating from the start again).

This saves individial memory allocations (which remember, are slow), and makes us have to worry less about
managing memory since we only have to worry about a couple of different “chunks” of memory instead of small
pieces used for individual arrays.

## The Implementation

### Clarification

Before we start, I almost always include this file at the top of my code when I use C.

It simply `typedef`s some built-in types and has some useful, commonly used utilities.
I include `.c` files directly, since I use unity builds for faster compilation.

You can easily write the header files for this if you prefer that.

```
// types.c
#ifndef TYPES_C
#define TYPES_C

#include <stddef.h>
#include <stdint.h>

typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;

typedef int8_t i8;
typedef int16_t i16;
typedef int32_t i32;
typedef int64_t i64;

typedef float f32;
typedef double f64;

typedef size_t usize;

#define KiB(x) x * 1024
#define MiB(x) KiB(x) * 1024
#define GiB(x) MiB(x) * 1024

#define MIN(a, b) a < b ? a : b
#define MAX(a, b) a > b ? a : b

#endif // TYPES_C
```

### `arena.c`

Here is the actual arena implementation.

We start with the include guard (again, because of the unity build), and include
some `libc` headers we will use:

```
#ifndef ARENA_C
#define ARENA_C

#include "types.c"
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

// code goes here

#endif // ARENA_C
```

Here is the arena struct definition:

```
struct arena {
    void *data;
    usize size;
    usize used;
};
```

It keeps track of how much memory was used. This could also be achieved by using a pointer
that points to the start of the region in memory that is still unused.

The `size` field is needed to understand if we are overflowing the arena when allocating
(that could very well happen if you are using too much memory).

And `data` simply points to the start of the arena.

So far so good, but now we need a way to create a new arena. Here it is:

```
struct arena_create_out {
    struct arena arena;
    bool success;
};
[[nodiscard]]
struct arena_create_out arena_create(usize size) {
    struct arena_create_out out = {0};
    out.arena.data = malloc(size);
    if (out.arena.data == NULL)
        return out;
    out.arena.size = size;
    out.arena.used = 0;
    out.success = true;
    return out;
}
```

When returning multiple values, instead of using output parameters, which I find
confusing and it makes the API unintuitive in my opinion, I much prefer to return
structs.

We then need a way to allocate some memory in this arena.
Before this, define this macro (I have it at the top of the file, but I am
introducing it now since this is when it becomes relevant):

```
#define ARENA_ALIGN 8
```

This will be used to align the data.
For some reason, our computers are much better at dealing with data that is aligned
at 8 bytes. This means that the start of each new allocation has to be at a multiple of 8 bytes.

`malloc` handles this for us, but since we are not allocating with `malloc` now, but
simply reserving some space in an already allocated chunk of memory, we have
to align the data ourselves to make for faster accessing.

Here is the allocation function:

```
struct arena_alloc_out {
    void *ptr;
    bool success;
};
[[nodiscard]]
struct arena_alloc_out arena_alloc(struct arena *arena, usize size) {
    struct arena_alloc_out out = {0};
    usize alignment = (ARENA_ALIGN - (arena->used % ARENA_ALIGN)) % ARENA_ALIGN;
    out.ptr = arena->data + arena->used + alignment;
    if (out.ptr + size > arena->data + arena->size)
        return out;
    arena->used += size + alignment;
    out.success = true;
    return out;
}
```

Here, we understand how much “padding” we need to add (the `alignment` variable)
at the end of our allocation such that the next one will be aligned to 8 bytes.

I use the pattern of having a `success` field in the return type of the function,
and on error I simply return early (since it is initialized as all-zeroes with
`= {0}`, `success` will be `false`).

If you don’t like this approach, it should be fairly trivial to migrate
the API to a different convention.

Most of the work is done, I created a simple wrapper to change the size of
an allocation. It simply allocates at the end of the arena with the new size
and copies the data over. The old data is left untouched as it will be
freed once the arena is freed (remember, we cannot free individual allocations,
it’s either the whole arena or nothing).

You don’t *really* need it, but I found that it made my code cleaner, so here it is:

```
struct arena_realloc_out {
    void *ptr;
    bool success;
};
[[nodiscard]]
struct arena_realloc_out arena_realloc(struct arena *arena, usize new_size, void *src, usize old_size) {
    struct arena_realloc_out out = {0};
    struct arena_alloc_out alloc_out = arena_alloc(arena, new_size);
    if (!alloc_out.success)
        return out;

    out.ptr = memcpy(alloc_out.ptr, src, MIN(old_size, new_size));
    out.success = true;
    return out;
}
```

If the new size is smaller than the new size, then a part of the data is cut off at the end.
Also, internally `realloc` knows the size of each allocation associated with each pointer,
so you do not need to pass the old size, only the new size.

Since our arena implementation does not keep track of the size of allocations in any way,
we will have to pass the old size to the function separately. The user of the arena
is expected to know the size of each allocation.

This could also be implemented, but I would like to keep it simple.

The next two functions are small wrappers, they’re only there for convenience but are not
essential:

```
void arena_clear(struct arena *arena) { arena->used = 0; }

void arena_free(struct arena *arena) { free(arena->data); }
```

This is it for my implementation.

## Example Usage

I have a simple string library that I have implemented for my own use cases.
It has two concepts: a string view and an owned string.

An owned string is NUL-terminated and `owns` its data (i.e. it has to be
allocated and freed). That also means that one can modify an owned string directly.

A string view only *views* the data, so you cannot modify it directly, since there may exist
multiple string views viewing the same data and modifying it would lead to unexpected changes
to the other strings. On the other hand, only one owned string shall exists for any given region
of data.

What is important is that I need a way of converting string views to owned strings.
To do this, we have to create a new owned string (which means allocating the memory for it),
and copy the data over. Again, this is to make sure that changes to this owned string
do not interfere with other potential string views viewing it.

We also add a NUL-terminator at the end of the owned string (to make it compatible
with other legacy C APIs).

To do this, we pass an arena allocator to the function and allocate to that arena:

```
struct str_view {
    const u8 *data;
    usize len;
};

struct str_owned {
    u8 *data;
    usize len;
};

struct str_view_to_str_owned_out {
    struct str_owned str;
    bool success;
};
[[nodiscard]]
struct str_view_to_str_owned_out str_view_to_str_owned(
    struct arena *arena,
    struct str_view s
) {
    struct str_view_to_str_owned_out out = {0};
    struct arena_alloc_out alloc_out = arena_alloc(arena, s.len + 1);
    if (!alloc_out.success)
        return out;
    out.str.data = alloc_out.ptr;
    memcpy(out.str.data, s.data, s.len);
    out.str.len = s.len;
    out.str.data[out.str.len] = 0;
    out.success = true;
    return out;
}
```

This does two things:

1. The caller immediately knows that this function allocates memory (since its first argument is an arena pointer)
2. The caller can decide where (i.e. with which lifetime) to allocate the memory.

Regarding point no. 2, since the caller decides the lifetime by passing the relevant arena,
they do not have to worry about freeing the memory directly,
the memory is simply freed once the lifetime is over and the arena is discarded (or cleared).
This could be at the end of the program (in which case freeing the memory would be unnecessary,
since the operating system handles it for us), or at the end of the function,
or whenever they are sure that the memory is not needed anymore.

## Conclusion

I recently started including arenas in my programming habits and I must say that I have been really satisfied.
I have migrated my string library to use arenas and I think you could benefit a lot from using arenas as well.

Sometimes the gains in developer experience are larger than in other cases, but I would say that having this
tool under your belt as a programmer is definitely a net benefit.

I don’t claim to be an expert and this implementation is just something that I have built
myself for personal use. It may not be perfect, but it should serve as a nice introduction to get you started.

Thanks for reading and I hope you found this useful!
