# Announcing Redis 8.8: New array data structure, rate limiter, performance improvements, & more

**Tagline:** News & Media | **Authors:** Lior Kogan | **Categories:** Tech | **Published:** 2026-06-02 | **Updated:** 2026-06-02

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.

## Summary of performance improvements in 8.8

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.

## Summary of new features in 8.8

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](https://github.com/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](https://github.com/raffertyyu), together with the Redis team).

Our investment in improving **Redis Streams** continues.

- [Redis 8.2](https://redis.io/blog/redis-82-streams-bitmap/) simplified message acknowledgment and deletion across multiple consumer groups

- [Redis 8.4](https://redis.io/blog/redis-8-4-open-source-ga/) made it easier for consumers to read both new and idle pending messages

- [Redis 8.6](https://redis.io/blog/announcing-redis-86-performance-improvements-streams/) introduced idempotent production

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](https://redis.io/blog/announcing-redis-community-edition-and-redis-stack-74/) 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](https://redis.io/docs/latest/develop/pubsub/keyspace-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](https://redis.io/blog/redis-8-4-open-source-ga/) 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.

## The new features explained

### Array: A new general-purpose data structure

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:

- **An array doesn’t need to have a fixed size
**Arrays grow and shrink dynamically. Elements can be set at any index (0 to 2⁶⁴−1), and the array grows efficiently as needed. Elements can be deleted and the array shrinks accordingly.

- **An array can be dense or sparse
**The used indices don’t have to be consecutive, and yet the memory footprint is proportional to the number of elements, and access by index remains extremely fast.
For example: an array index can represent a product ID and the values hold product names or details. Similarly, the index can represent timestamps and the values hold log events.

- **An array can be used as a ring buffer (Sliding Window)
**Arrays can act as a bounded rolling buffer: retain the last *n* elements, maintain insertion order, and automatically overwrite older entries.
Think of a log file or a stream of events, where you want to efficiently keep the last *n* log entries, events, or measurements, and frequently fetch the last [up to] *n* values. This is especially useful if you need to feed a rule engine, process a fraud detection window, continuously update a chart, or execute security validations.

- **Arrays can aggregate data
**When the values are numeric (for example, real-time sensor reports or stock quotes), arrays support server-side computation over index ranges, including `SUM`, `MIN`, and `MAX`. When the values are binary flags, Boolean aggregators (`AND`, `OR`, `XOR`) are supported as well.
Server-side aggregators are Ideal for sensor data, financial ticks, and real-time metrics. When combined with ring buffer semantics, Arrays enable sliding window analytics such as real-time anomaly detection.

- **An array can be searched
**An array can represent a textual file (e.g., .txt, .csv, .log), where each element is indexed by the line-number and holds a single text line. Users can iterate over these lines for analysis, and can search for specific lines using an exact or partial string, a glob-style pattern, or a regular expression.
With ring buffer semantics, arrays can constantly hold the last *n* log-lines allowing users and agents to contextualize or enrich incoming events based on recent ones.

In summary, an array is a **dynamic, flexible, high-performance, index-addressable, compute-aware container **that combines aspects of:

- List (ordered data)

- Time-series (sliding windows)

- Sparse map (non-contiguous indices)

- Analytical engine (aggregation and search)

**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:

- You need extremely fast access by index or by index-range

- You need a sliding window over recent data

- You need server-side aggregation

- You want to search for matching elements

**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/](https://redis.io/docs/staging/DOC-6334/develop/data-types/arrays/)

Array commands: [https://redis.io/docs/latest/commands/?group=array](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/](https://marketing.redis.io/blog/diving-deep-into-rediss-new-array-data-type/)

## Window counter rate limiter

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:

```
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 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:

1. `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.

2. When `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.

3. Boundary enforcement: the request is rejected if it would exceed the defined bounds. With `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.

## Streams: NACKing messages

In real-world applications, stream consumers don’t always successfully process the messages they consume. Failures can happen for many reasons:

- A consumer may encounter internal issues unrelated to the message itself. For example, failing to reach an external service it needs for processing the message.

- A consumer may need to shut down and release unprocessed messages.

- A resource-constrained consumer (CPU, memory) may be unable to process certain messages (at least, in a timely manner).

- A message may be malformed, poisoned, or even malicious.

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:

- Marked as unowned (its last consumer is set to an empty string)

- Assigned a last delivery time of 0

- Placed at the end of the NACKed portion of the PEL

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:

- When `CLAIM min-idle-time` is specified:

  - NACKed messages (*new behavior*)

  - Messages pending for at least min-idle-time

  - Never-delivered messages

- If `CLAIM` is not specified:

  - Only never-delivered messages are returned (*unchanged behavior*)

## Hash: Subkey notifications

Redis [key-level notifications](https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/) let clients subscribe to key-related events in real time via pub/sub channels. There are two types of channels:

- **Keyspace notifications**: clients subscribe to a specific key; each message contains an event type.

- **Keyevent notifications**: clients subscribe to specific events; each message contains a key name.

In [Redis 7.4](https://redis.io/blog/announcing-redis-community-edition-and-redis-stack-74/), 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:

- **Subkeyspace notifications**: Clients subscribe to a specific key; each message contains an event type and field names.

- **Subkeyevent notifications**: Clients subscribe to a specific event type; each message contains a key name and field names.

- **Subkeyspaceitem notifications**: Clients subscribe to a specific key+field combination; each message contains an event type.

- **Subkeyspaceevent notifications**: Clients subscribe to a specific event+key combination; each message contains field names.

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`.

## Time series: Multiple aggregators in a single command

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.

## JSON: Explicitly declaring floating-point array types

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]**`

- **FPHA** stands for *Floating-Point Homogeneous Array*

- It forces Redis to store any floating-point array in value using the specified format

- For large arrays, such as embeddings with hundreds or thousands of elements, the difference becomes substantial, often reducing memory usage by multiple times.

## Sorted sets: Union and intersection - COUNT aggregator

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`:

- If no weights are specified, the score becomes the **number of input sets containing the element** (i.e., `1 + 1 + ...`)

- If weights are specified, the score becomes the **sum of the weights of the sets containing the element** (i.e., `weight₁ + weight₂ + ...`)

This effectively ignores the original element scores and focuses only on **set membership**.

The `COUNT` aggregator enables patterns such as:

- Ranking items by popularity across multiple sources

- Finding elements that appear in many datasets

- Implementing voting or scoring systems based on presence rather than value

All without requiring additional client-side logic.

## Getting started

All these enhancements are generally available on Redis 8.8 today. You can start using the new commands by [downloading Redis 8.8](https://redis.io/downloads/) and experimenting with them in your existing workflows.

Have feedback or questions? Join the discussion on our [Discord server](https://discord.gg/redis) or reach out to your account manager.