Redis 8.8 in Redis Open Source is now available, bringing performance improvements alongside a set of powerful new features. Highlights include array - a new general-purpose data structure, a window counter rate limiter, streams message NACKing, subkey notifications for hash fields, explicit control over JSON numeric array storage, multiple aggregators in a single time series query, and a new COUNT aggregator for sorted sets union and intersection.
Redis 8.8 introduces significant end-to-end throughput improvements:
| Data type | Operations | End-to-end throughput improvements |
|---|---|---|
| Strings | MGET (pipelined, with I/O-threads) | Up to 68% |
| MGET (pipelined, single thread) | Up to 50% | |
| MSET | Up to 8% | |
| Hash | HGETALL | Up to 25% (1K+ fields) |
| Streams | XREADGROUP | Up to 83% (COUNT 100) |
| Sorted set | ZADD, ZINCRBY, ZRANGEBYSCORE | Up to 74% |
| Bitmap | Bitmap operations | Up to 28% (x86) |
| HyperLogLog | PFCOUNT | Up to 18% (x86) |
| (several) | SCAN, HSCAN, SSCAN, ZSCAN | Up to 40% |
In addition, persistence and replication (full synchronization) is now up to 60% faster.
Redis has always been about choosing the right data structure for the job. In Redis 8.8, we introduce a new general-purpose data structure: array. An array is an index-addressable collection of string values. Each array element is stored at a numeric index, and can be accessed extremely fast. Arrays are dynamic, sparse-friendly, and compute-aware containers, enabling new use cases and better flexibility and efficiency for existing use cases (by @antirez).
Rate limiting is one of the most common Redis use cases. Traditionally, users implemented rate limiters using server-side Lua scripts combined with client logic. In Redis 8.8, we introduce a window counter rate limiter (by @raffertyyu, together with the Redis team).
Our investment in improving Redis Streams continues.
Building on this momentum, Redis 8.8 adds support for message NACKing, allowing consumers to explicitly release pending messages so they become immediately available and prioritized for consumption by other consumers.
In Redis 7.4 we introduced hash field expiration – a capability that saw strong adoption. A frequent follow-up request was for field-level notifications, similar to existing key-level notifications. Redis 8.8 delivers this with subkey notifications for hash fields, allowing clients to subscribe to events such as field expiration and deletion. These notifications include the key, the subkey (field name), and the event type.
Retrieving multiple time series aggregators is a common operation. For example, candlestick charts rely on MIN, MAX, FIRST, and LAST aggregations. Prior to Redis 8.8, this required multiple commands. Redis 8.8 now supports multiple aggregators in a single time series command, reducing round trips and simplifying client logic.
Redis 8.4 introduced support for homogeneous numeric arrays in JSON, delivering up to 92% memory reduction – especially valuable for AI workloads. In Redis 8.8, users can now explicitly control how numeric arrays are stored (BF16, FP16, FP32, or FP64), enabling better alignment with source data, vector indexing needs, and memory/precision tradeoffs.
Finally, Redis 8 extends sorted set union and intersection operations with a new COUNT aggregator. This allows the score of each element to reflect either the number of input sets it appears in or the weighted sum across those sets, unlocking new use cases in ranking, scoring, and analytics.
Redis has always been about choosing the right data structure for the job. Redis traditionally provides several core data structures, including lists, hashes, sets, and sorted sets. In Redis 8.8, we introduce a new general-purpose data structure: array.
What is an array?
An array is an index-addressable collection of string values. Each element is stored at a numeric index, and can be accessed extremely fast.
Arrays go far beyond basic indexed storage. They are flexible, memory-efficient, and compute-aware. Arrays have some capabilities that enable new use cases and better flexibility and efficiency for many existing use cases:
SUM, MIN, and MAX. When the values are binary flags, Boolean aggregators (AND, OR, XOR) are supported as well.In summary, an array is a dynamic, flexible, high-performance, index-addressable, compute-aware container that combines aspects of:
Random element access: Array vs list vs hash
Benchmarking arrays against the closest list and hash equivalents under random access at large element counts, the advantages of array show up clearly:
| Operation (100K elements; 1 KB values) | Array | List | Hash |
|---|---|---|---|
| Read random element | 675K ops/sec | 133K ops/sec | 626K ops/sec |
| Write random element | 757K ops/sec | 137K ops/sec | 689K ops/sec |
| Delete random element | 841K ops/sec | — | 730K ops/sec |
* Redis 8.8, single instance on an Intel Sapphire Rapids m7i.metal-24xl machine
For random-element operations, array provides 8-15% better throughput than Hashes and are at least 5 times faster than Lists.
Memory wise, lists are the most compact. Arrays require ~18% more memory per element, while hashes require 30-46% more memory than lists, depending on the size of the elements:
| Element size (100K elements) | Array | List | Hash |
|---|---|---|---|
| 100 bytes | 122 bytes/element | 104 bytes/element | 151 bytes/element |
| 1 Kbyte | 1290 bytes/element | 1035 bytes/element | 1337 bytes/element |
Ring buffer: Array vs list
A common pattern in Redis is using a list as a bounded ring buffer: clients push new entries with RPUSH and trim list back with LTRIM to keep a constant number of elements. Arrays expose ARRING, which performs the same operation in a single atomic command.
| Ring size; element size | Array (ARRING) | List (RPUSH+LTRIM) | Array’s advantage |
|---|---|---|---|
| 1K elements; 100 bytes | 1.11M inserts/sec | 512K inserts/sec | × 2.2 |
| 100K elements; 100 bytes | 1.12M inserts/sec | 528K inserts/sec | × 2.1 |
| 1K elements; 1 Kbyte | 840K inserts/sec | 424K inserts/sec | × 2.0 |
| 100K elements; 1 Kbyte | 837K inserts/sec | 413K inserts/sec | × 2.0 |
* Redis 8.8, single instance on an Intel Sapphire Rapids m7i.metal-24xl machine
ARRING delivers twice the throughput (inserts/sec) compared to the equivalent RPUSH+LTRIM idiom, independent of ring size. Memory footprint is the same as above: Arrays require ~18% more memory than lists.
When should arrays be used?
Arrays are extremely useful when:
What arrays are not suitable for?
Arrays are not a replacement for other data structures.Use lists if you need push/pop operations, or inserting elements between others.
Use hashes if you need field name-based access instead of numeric indices.
Where can I learn more?
Array documentation: https://redis.io/docs/staging/DOC-6334/develop/data-types/arrays/
Array commands: https://redis.io/docs/latest/commands/?group=array
Diving deep into Redis’s new array data type: https://redis.io/blog/diving-deep-into-rediss-new-array-data-type/
Window counter rate limiters, including fixed window, fixed window with lazy reset, and sliding window counter variants, use one or more fixed-duration time windows. Each window maintains a counter initialized to 0 when the window is created, along with a maximum capacity representing the number of tokens allowed during that window’s lifetime.
Before Redis 8.8, implementing a Window counter rate limiter required Lua scripting. In 8.8, we introduce a new command for working with window counters:
The idea is simple: each window has a duration (specified via EX or PX) and a token capacity (specified with UBOUND). The number of tokens requested can be specified with BYINT increment (default is 1). INCREX attempts to increment the counter by the requested number of tokens. The key is created if it does not already exist.
To make this command suitable for rate limiter use cases, beyond basic increment semantics, INCREX introduces three new capabilities compared to the existing INCR family of commands:
INCREX returns both the new counter value and the actual increment applied, allowing the caller to immediately determine whether the request should be allowed or rejected.ENX is specified, expiration is set only if the key does not already have one. This ensures that the window’s TTL is set only when a window is created and not modified on subsequent requests during its lifetime.SATURATE, the request may be “partially accepted” with the counter clamped to the specified bounds (“saturated”) .Beyond rate limiting, INCREX can be seen as a generalized form of INCR, INCRBY, INCRBYFLOAT, as well as DECR and DECRBY (via negative increments), with added support for bounds and expiration control.
In real-world applications, stream consumers don’t always successfully process the messages they consume. Failures can happen for many reasons:
Before Redis 8.8, consumers had no way to explicitly reject (NACK) a message. They could either acknowledge it or leave it pending. In practice, this meant other consumers in the consumer group had to recover these messages using XREADGROUP … CLAIM, XPENDING+XCLAIM or XAUTOCLAIM.
This approach introduces delays, since messages remain idle in the Pending Entries List (PEL) until another consumer claims them – an issue for time-sensitive systems.
Redis 8.8 introduces a new command to address this directly:
XNACK key group [SILENT|FAIL|FATAL] IDS numids id [id ...]
This command allows consumers to explicitly release messages back to the stream, making them immediately available for re-delivery.
XNACK supports three modes, each designed for a different real-world scenario:
SILENT - Used when the failure is unrelated to the message (e.g., shutdown or transient internal errors). The delivery counter is decremented by 1, effectively undoing the increment that occurred when the message was added to the PEL.FAIL - Used when the message cannot be processed by this consumer but may succeed elsewhere (e.g., requires more resources). The delivery counter remains unchanged (it was already incremented by 1 when added to the group's PEL).FATAL - used for malformed, poison, or potentially malicious messages. The delivery counter is set to LLONG_MAX, making it easy to detect and route to a dead-letter queue.These modes map naturally to production scenarios: graceful shutdowns or transient failures, resource-based failures, and poison message handling.
When a message is NACKed, it is:
The head of the PEL is reserved for all NACKed messages, ordered FIFO among themselves, followed by pending messages that were neither ACKed nor NACKed in their existing order. This guarantees that NACKed messages are always prioritized over idle pending messages.
The delivery order on XREADGROUP is updated accordingly:
CLAIM min-idle-time is specified:CLAIM is not specified:Redis key-level notifications let clients subscribe to key-related events in real time via pub/sub channels. There are two types of channels:
In Redis 7.4, we introduced hash field expiration. This feature saw strong adoption, and a common request followed: support for hash field-level notifications, since key-level notifications do not include field names.
Redis 8.8 introduces subkey-level notifications. Starting with Hashes, clients can now subscribe to events at the field level, such as field updates, deletions, and expirations.
Subkey notifications include the key, subkeys (for hashes, these are field names), and the event type.
Redis 8.8 adds four new channel types:
These mirror the flexibility of keyspace notifications while extending visibility down to the field level.
The following events are emitted for hash fields: hset, hdel, hexpire, hexpired, hpersist, hincrby, and hincrbyfloat.
The TS.RANGE, TS.REVRANGE, TS.MRANGE, and TS.MREVRANGE commands support an optional AGGREGATION parameter which allows grouping samples into time buckets and applying an aggregation function.
Users can choose from 15 supported aggregators (such as AVG, SUM, MIN, MAX, FIRST, and LAST), and the results are computed accordingly.
In many real-world scenarios, however, multiple aggregations are needed simultaneously. A common example is candlestick charts, which require MIN (low), MAX (high), FIRST (open), and LAST (close).
Before Redis 8.8, this required issuing multiple commands - one per aggregator - resulting in additional latency and client-side complexity.
Redis 8.8 introduces support for multiple aggregators in a single command, allowing all required aggregations to be computed in one request.
The command syntax remains unchanged. Users can now specify multiple aggregators as a comma-separated list:
TS.RANGE key from to AGGREGATION MIN,MAX,FIRST,LAST bucketDuration
Note that aggregators are comma-separated, with no spaces between them.
The JSON specification defines a generic “number” type, without enforcing a specific representation such as IEEE-754 FP16, FP32, or FP64 for non-integers. As a result, each implementation must choose how to represent numeric values internally.
Starting with Redis 8.4, JSON numeric arrays (such as vector embeddings) are stored using efficient binary representations, significantly reducing memory usage. Redis automatically selects the most appropriate numeric type, but for non-integers, and without additional hints, this usually defaults to FP64 to preserve precision.
For example, the decimal value 0.3 cannot be represented exactly in binary (similar to how 1/3 cannot be represented exactly in decimal). To avoid loss of precision, Redis typically uses FP64. In practice, this means that many floating-point arrays end up being stored as FP64, even when such high precision is not required.
In many real-world scenarios, the original data was already generated using lower-precision formats. Redis 8.8 addresses this by allowing users to explicitly control how floating-point arrays are stored. Users can now choose between BF16, FP16, FP32, and FP64, enabling better alignment with source data, vector indexing requirements, and memory/precision tradeoffs.
The JSON.SET command includes a new optional parameter:
JSON.SET key path value [NX | XX] [FPHA BF16|FP16|FP32|FP64]
Sorted sets support set operations via ZUNION, ZUNIONSTORE, ZINTER, and ZINTERSTORE. For all four commands, users can control how element scores are computed in the result using the SUM, MIN, or MAX aggregators, optionally applying weights to each input set.
In some use cases, however, the original scores are not relevant. Instead, users may want the resulting score to reflect how many input sets contain each element, or, when weights are provided, the sum of the weights of the sets that contain it.
In Redis 8.8, we introduce a new COUNT aggregator to support this directly.
With COUNT:
1 + 1 + ...)weight₁ + weight₂ + ...)This effectively ignores the original element scores and focuses only on set membership.
The COUNT aggregator enables patterns such as:
All without requiring additional client-side logic.
All these enhancements are generally available on Redis 8.8 today. You can start using the new commands by downloading Redis 8.8 and experimenting with them in your existing workflows.
Have feedback or questions? Join the discussion on our Discord server or reach out to your account manager.