필사 모드: Redis 8.8's Array Type — The First New Core Data Structure Since Streams, and What Valkey Doesn't Have
English- Introduction — Two Releases, Six Days Apart
- The First New Core Type Since Streams
- What Array Actually Is
- Internal Representation — Slices, Pointer Tagging, Super-Directories
- 18 Commands
- ARLEN and ARCOUNT Count Different Things
- What the Vendor Benchmark Says — and Doesn't
- The Docs Still Call It Preview
- Valkey Doesn't Have It
- The Rest of 8.8 — INCREX May Be More Practical
- Built by AI, on the Record
- So, Should You Use It?
- Closing
- References
Introduction — Two Releases, Six Days Apart
In May 2026, two projects that split from the same root shipped releases almost at the same time. Valkey 9.1.0 on May 19, Redis 8.8.0 on May 25. Six days apart.
Read the two release notes side by side and the difference in temperature is obvious. Valkey 9.1.0 is the first stable 9.1 release, and it describes itself as "Upgrade urgency LOW" — the new features are a cluster-bus traffic metric and a fix for a latency spike during rehashing, and the rest is three CVEs and bug fixes. Redis 8.8.0's list is a different animal — right at the top it reads: "New data structure: Array (@antirez)".
This isn't about fork drama. It's about a concrete difference you can confirm in the source tree. And that difference exists as a single line inside src/server.h.
The First New Core Type Since Streams
Redis's core object types are baked into server.h as constants. Here's the relevant section as of the Redis 8.8.0 tag.
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
#define OBJ_MODULE 5 /* Module object. */
#define OBJ_STREAM 6 /* Stream object. */
#define OBJ_ARRAY 7 /* Array object. */
OBJ_ARRAY 7 is the newly added line. The one above it, OBJ_STREAM 6, is the stream, and if you look at XADD's command definition, its since is 5.0.0. In other words, this is the first time the core object type count has grown since Streams in Redis 5.0, back in 2018.
There's a common misconception worth addressing here. "Didn't vector sets land in 8.0?" — true, but vector sets aren't a core type. VADD's definition lives in modules/vector-sets/commands.json, its group is vector_set, and the implementation is modules/vector-sets/hnsw.c. That is, it's a module shipped inside the Redis tree, not a t_*.c core type. Array, by contrast, went straight into the core as src/t_array.c (about 73KB) and src/sparsearray.c (about 80KB).
What Array Actually Is
The official docs define it this way — Array is a sparse index-addressed data structure that maps integer indices in the range 0 to 2⁶⁴−1 to string values. Unlike a list, you don't access it by sequential position; you access it directly by index, and you can set any index without allocating the empty slots in between.
antirez writes out the "why" himself in the body of PR #15162. In his own words, every existing type falls a bit short whenever "the position itself carries business meaning" — hashes give you random lookup but you have to store the index separately as a key, with no range visibility; lists give you appends and trimming but make it hard to reach into the middle; streams are a different animal entirely, append-only events. Slot 37, step 4, row 18552, line 11 of a file — those are the examples he gives.
Put together, here's the territory Array is aimed at:
- Data where the index is itself the domain key (time slots, seat numbers, row numbers, step numbers)
- Sparse data where values exist only here and there (missing sensor readings, annotations attached to only some rows)
- Ring buffers that keep only the most recent N items
Internal Representation — Slices, Pointer Tagging, Super-Directories
antirez's encoding write-up in the PR body breaks down into four layers.
- When dense, Array is essentially a fancier C array. It doesn't pay any separate cost to store the index itself.
- Rather than going fully flat, though, it's split into slices of 4096 elements each, and each slice uses a special sparse encoding when it holds few elements. When a slice is empty, only a
NULLremains in the directory. - Small integers, floats, and short strings are pointer-tagged, so they consume no extra memory beyond the pointer slot itself.
- When it gets very sparse, it uses a super-directory of windowed directories. This representation only kicks in once the element count crosses 8 million, or once a very high index has been set.
Why layer 4 exists is explained in antirez's dev log. He writes that he wanted ARSET myarray 293842948324 foo to work without a giant allocation even from a user, and that he realized mid-implementation that his original two-tier structure (directory + slices) wasn't enough. So he redesigned it so that, under certain conditions, the structure internally reshapes itself into a super-directory of sliced dense directories, with the goal of making ARSCAN run in time proportional to the number of elements that actually exist, not the width of the range.
There's something worth being honest about here. That goal is about the "typical case," not a guarantee. The official complexity notation for ARSCAN and AROP reads — "O(P) where P is visited positions in touched slices (dense scanned slots + sparse entries), with worst-case O(|end-start|+1) and typical case close to O(N)". The worst case is proportional to the width of the range. The fact that the docs write this caveat out themselves is, if anything, a signal you can trust.
18 Commands
There are exactly 18 AR* commands in src/commands/ as of the Redis 8.8.0 tag.
ARSET ARGET ARMSET ARMGET ARGETRANGE ARSCAN
ARDEL ARDELRANGE ARLEN ARCOUNT ARINFO
ARINSERT ARNEXT ARSEEK ARRING ARLASTITEMS
AROP ARGREP
(antirez's dev log mentions an ARPOP, but it isn't in the final command list for 8.8.0. Don't take a name mentioned in a blog post at face value — checking the tagged source is the safer move.)
Grouped by character, they break down like this.
Basic read/write. ARSET writes a run of consecutive values starting at a given index. ARGET reads one index, returning nil for an unset index. ARMSET and ARMGET handle multiple index-value pairs in one call. All of these are @fast.
The cursor family. Array is mostly stateless, but it remembers exactly one thing: the index of the last item appended. ARINSERT writes at that cursor position and advances the cursor, ARNEXT reports the index that will be used next, and ARSEEK moves the cursor to a chosen position.
Ring buffer. ARRING takes a size and automatically wraps and trims once it overflows. ARLASTITEMS returns the most recently added N items, and REV reverses the order.
Server-side aggregation. AROP runs SUM, MIN, MAX, AND, OR, XOR, plus MATCH and USED over a range.
Search. ARGREP scans elements in a range against a text predicate. There are four predicate kinds — EXACT, MATCH, GLOB, RE — multiple predicates can be combined with AND or OR, and LIMIT, WITHVALUES, and NOCASE options are available.
Here's an actual worked example from the docs.
ARSET events:1 0 "login" "click" "purchase" -> 3
ARGET events:1 0 -> "login"
ARGET events:1 999 -> nil
ARRING readings 3 "v0" -> 0
ARRING readings 3 "v1" -> 1
ARRING readings 3 "v2" -> 2
ARRING readings 3 "v3" -> 0 # exceeds size 3, wraps back to index 0
ARGET readings 0 -> "v3" # overwrites the oldest entry, v0
ARLASTITEMS readings 3 -> ["v1", "v2", "v3"]
ARLASTITEMS readings 3 REV -> ["v3", "v2", "v1"]
ARLEN and ARCOUNT Count Different Things
This is the first landmine you'll step on in practice. Here's the docs' own example, carried over as-is.
ARSET sparse 0 "a" -> 1
ARSET sparse 1000000 "b" -> 1
ARLEN sparse -> 1000001
ARCOUNT sparse -> 2
ARLEN is "max index + 1"; ARCOUNT is "number of non-empty elements." Both are O(1), but they mean entirely different things. Reach for ARLEN with the mental model of a list's LLEN and you'll get a million out of an Array holding two elements. It's an entirely reasonable definition for a sparse data structure — but a reasonable definition is exactly the kind of thing that catches people at three in the morning.
ARGETRANGE versus ARSCAN follows the same shape — ARGETRANGE seq 0 3 fills the gaps with nil, giving you ['a', 'b', None, 'd'], while ARSCAN seq 0 3 returns only what exists, paired with its index.
What the Vendor Benchmark Says — and Doesn't
These are the numbers Redis published in its 8.8 announcement post. All of them are vendor self-measured, on a single Redis 8.8 instance, Intel Sapphire Rapids m7i.metal-24xl, with 100,000 elements.
| Operation (100,000 elements, 1KB value) | Array | List | Hash |
|---|---|---|---|
| Random element read | 675K ops/sec | 133K ops/sec | 626K ops/sec |
| Random element write | 757K ops/sec | 137K ops/sec | 689K ops/sec |
| Random element delete | 841K ops/sec | — | 730K ops/sec |
Redis's own summary is "for random-element operations, Array delivers 8–15% better throughput than Hash and is at least 5x faster than List." Here's the ring-buffer comparison.
| Ring size / element size | Array (ARRING) | List (RPUSH+LTRIM) |
|---|---|---|
| 1K / 100B | 1.11M inserts/sec | 512K inserts/sec |
| 100K / 100B | 1.12M inserts/sec | 528K inserts/sec |
| 1K / 1KB | 840K inserts/sec | 424K inserts/sec |
| 100K / 1KB | 837K inserts/sec | 413K inserts/sec |
An honest reading of this table goes like this.
The gain over Hash isn't large. 8–15% is a number a vendor measured on its own hardware while promoting its own new feature. If you're already using Hash and it's working, that number alone isn't a reason to migrate.
5x over List is a lopsided comparison. "Random element read" on a List is, by nature, an O(N) linked-list traversal. That 5x comes from asking a List to do something it was never meant to do — it doesn't mean List is slow. The 2x on ARRING is a fairer comparison — it collapses two round trips of RPUSH+LTRIM into a single atomic command, which matches how people actually use this pattern.
Array uses more memory. According to the table in the same post, at 100-byte elements it's 122 bytes per element for Array, 104 for List, and 151 for Hash; at 1KB elements it's 1290 / 1035 / 1337 bytes respectively. In Redis's own words, Array uses about 18% more memory per element than List. Switch a ring buffer from LPUSH+LTRIM to ARRING and you get 2x the throughput in exchange for paying that much more memory.
And there are things missing from this table — memory figures for the sparse case, cluster environments, replication/AOF load, and numbers on ARM. The announcement post only shows a single x86 instance.
The Docs Still Call It Preview
This is the most important caveat, and it isn't in the announcement post — only in the docs. The first line of the official Array documentation reads:
Array is a new data type that is currently in preview and may be subject to change.
It shipped in a GA release, but the docs still flatly say "preview, and may be subject to change." Every command definition's since says 8.8.0, but that isn't a promise of API stability. Factor that one line into your calculus before you put a production data model on top of this.
There's one more caveat. To power ARGREP's regex, the TRE library has been vendored in as deps/tre (confirmable in 8.8.0's deps/ listing). antirez writes that he chose TRE for its guarantee against pathological time/space blowups on adversarial patterns, and that he personally optimized inefficient alternation (patterns like foo|bar|zap) and fixed a few potential security issues. Even so, the plain fact remains — a new native C regex engine has entered the server, and it runs directly on user-supplied patterns. The automated review bot attached to the PR flagged exactly this as "new native code surface."
Valkey Doesn't Have It
Back to where this post started. In Valkey 9.1.0's tagged src/server.h, the object types end at OBJ_STREAM 6. There is no OBJ_ARRAY. src/ has exactly six files — t_hash.c, t_list.c, t_set.c, t_stream.c, t_string.c, t_zset.c — and no t_array.c. There are no commands starting with AR in src/commands/ either.
Which means, as of May 2026, Redis has 7 core types and Valkey has 6. Two servers that forked from the same codebase now have different sets of data structures. This isn't a performance-tuning or configuration difference — it's a difference in the data model, and it runs one way: data modeled with Array cannot be moved to Valkey.
Licensing overlaps with this. Redis 8.8's LICENSE.txt is a triple license — RSALv2 / SSPLv1 / AGPLv3 — while Valkey's COPYING reads SPDX-License-Identifier: BSD-3-Clause. This background is covered separately in The 2026 Open Source License Shift. The short version: if your org picked Valkey to stay on BSD, Array was never on the table to begin with; and if Array is appealing to you, you're also signing up to accept that license. It isn't a feature choice — it's a bundle choice.
The Rest of 8.8 — INCREX May Be More Practical
Array is the headline, but for many teams, the thing in 8.8 that's immediately useful might be INCREX. Rate limiters have mostly been implemented with Lua scripts up to now, and 8.8 turns this into a command (implemented by raffertyyu and the Redis team).
INCREX key
[<BYFLOAT|BYINT> increment]
[LBOUND lowerbound] [UBOUND upperbound] [SATURATE]
[EX sec | PX msec | EXAT unix-time-sec | PXAT unix-time-msec | PERSIST]
[ENX]
The announcement post highlights three key points. First, it returns both the new counter value and the increment actually applied, so the caller can decide allow/deny immediately. Second, ENX sets an expiry only if the key doesn't already have one, so a window's TTL is pinned at window creation and doesn't keep getting pushed out by later requests — exactly the part everyone gets wrong at least once trying to do this in Lua by hand. Third, it rejects the increment if it would cross a bound, and with SATURATE it clamps to the bound and partially accepts instead. The rate-limiting algorithms themselves are covered in Rate Limiting Algorithms — Token Bucket and Sliding Window.
Beyond that, 8.8 also adds XNACK (with SILENT/FAIL/FATAL modes) for stream consumers to explicitly return a message, per-field notifications for hash fields, a COUNT aggregator for ZUNION/ZINTER, and FPHA for JSON.SET (specifying whether a float array is stored as BF16, FP16, FP32, or FP64).
Built by AI, on the Record
antirez doesn't hide how this type was built — he writes it out in his dev log. He started in the first week of January; the first month was spent hand-writing the spec document alone. He initially paired with Opus, then, once GPT-5.3 shipped, moved design and development to Codex. From the second month on, he implemented via automated coding while continuously reviewing, and the indirection redesign mentioned earlier came out of this period. The third month was stress testing.
His conclusion, in his own words, is that high-quality systems programming still requires him to be fully engaged, but that AI let him reach a level of complexity he would ordinarily have skipped. And ARGREP was never on the plan — it came from playing around with the data structure, stuffing markdown files into an Array, and it happened to line up with his own need for an agent skill knowledge base.
The scale of the PR itself is a matter of record — 18 commits, 86 files, +22,258 / −39 lines. It opened May 4 and merged May 13, and shipped in the 8.8.0 GA release 12 days after that. How you weigh this is up to you, but at minimum it's worth noting as a concrete, documented case of "AI-written code entering a production database as a core data structure." It's reasonable to keep this context in mind alongside the docs' preview label.
So, Should You Use It?
Here's how the decision breaks down.
When it earns its cost
- The index is genuinely a domain key — time slots, seat/berth numbers, row numbers, workflow steps. If you're currently stuffing the index into a Hash as a string field, this is exactly its spot.
- You're running a ring buffer with
RPUSH+LTRIM, and the round trips and non-atomicity actually hurt.ARRINGcollapses that into one command. - You're pulling range aggregation all the way to the client to do it there.
AROPfinishes it on the server.
When it's overkill or risky
- You need push/pop, or need to insert between elements → Redis itself tells you to use a list.
- You're accessing by field name rather than a numeric index → it tells you to use a hash.
- Memory is tight → it costs roughly 18% more per element than a list.
- You're using Valkey, or might → the type doesn't exist there.
- You need API stability → the docs say preview.
- You already have a hash-based design that works well → the vendor benchmark's 8–15% figure doesn't justify a rewrite.
Closing
To sum up: Redis 8.8 grew a core object type for the first time since Streams, and that type is a sparse container for data where the index carries meaning. It handles both dense and sparse cases through 4096-element slices and pointer tagging, and its 18 commands push aggregation and grep down onto the server itself. Valkey 9.1's source, released the same month, has no such type, and going forward the two servers stay split at the data-model level.
At the same time, this is a type that shipped in GA while its docs still call it preview, most of the vendor benchmark's gains come from asking a list to do something a list can't do well, and the real gain over Hash is a single-digit-to-teens percentage paid for with more memory.
So the conclusion isn't "Redis got something nice, let's use it." If you're currently stuffing an index into a hash key as a string, or living with LPUSH+LTRIM round trips, Array targets that exact discomfort. If you don't have that discomfort, what matters to you in 8.8 might be INCREX instead of Array — or nothing at all. A new data structure is a new hammer, and the first step is checking whether you actually have a nail.
References
- Redis 8.8.0 release notes (GitHub)
- Valkey 9.1.0 release notes (GitHub)
- Implement the new Redis Array type — PR #15162 (antirez)
- Redis array type: short story of a long development — antirez's dev log
- Announcing Redis 8.8 — vendor announcement post, source of the benchmark figures
- Redis arrays official docs — the preview label and the 18 commands
- TRE — the regex engine behind ARGREP
- In-memory Caching & KV Stores in 2026 (related post)
- The 2026 Open Source License Shift (related post)
- Rate Limiting Algorithms — Token Bucket and Sliding Window (related post)
현재 단락 (1/109)
In May 2026, two projects that split from the same root shipped releases almost at the same time. [V...