Large Kubernetes clusters can put significant memory pressure on the control plane when the API server needs to read a large collection of objects from etcd.
This becomes especially visible when the API server initializes or rebuilds its watch cache. A cluster with many Pods, large Custom Resources, or other high-volume resources can require the API server and etcd to process a large amount of data at the same time.
Kubernetes 1.37 introduces Beta support for etcd RangeStream. With etcd 3.7 or later, the API server can stream large result sets in chunks instead of receiving one large buffered response.
The goal is simple: reduce peak memory usage and make large reads more predictable.
Why Large etcd Reads Consume So Much Memory
Kubernetes stores its persistent API state in etcd.
When the API server needs a collection of objects, it ultimately needs to read that data from etcd when the data is not already available from its watch cache.
The traditional etcd Range operation builds the complete response before returning it.
Consider a large collection:
etcd
|
|-- Object 1
|-- Object 2
|-- Object 3
|-- ...
`-- Object 100000
|
v
Build complete response
|
v
Send response
|
v
API server decodes response
The complete response can require substantial memory.
During processing, multiple representations of the same data can exist at different stages:
etcd
|
+-- Stored values
|
+-- Range response
|
+-- Serialized response
|
`-- gRPC send buffers
|
v
API server
|
+-- Received response
+-- Decoded objects
`-- Watch cache data
This is why a large read can create a temporary memory spike even when the final Kubernetes objects would fit comfortably in memory.
Why Pagination Does Not Fully Solve the Problem
The API server already uses pagination in several read paths.
Instead of requesting every object at once, it can request a limited number of keys:
Page 1 -> 1,000 objects
Page 2 -> 1,000 objects
Page 3 -> 1,000 objects
...
That helps, but the page size is based primarily on the number of keys.
It does not know how large each object is.
Consider two pages:
Page A
1,000 small objects
= relatively small response
Page B
1,000 large objects
= very large response
Both pages contain the same number of keys, but their memory requirements can be dramatically different.
Large objects such as Pods with large specifications, CRDs, or other resource types can therefore make a supposedly bounded page much larger than expected.
There is another cost associated with paginated range operations in etcd. Each page can require work against the underlying key index to determine the total result count.
RangeStream changes the way this large collection is delivered.
What Is etcd RangeStream?
etcd 3.7 introduces a server-streaming RangeStream RPC.
It accepts the same basic RangeRequest concept as the traditional Range operation but returns the results in chunks.
The difference looks like this:
Traditional Range
etcd
|
|-- Build entire response
|
`---------------> API server
RangeStream
etcd
|
|-- Chunk 1 ------> API server
|-- Chunk 2 ------> API server
|-- Chunk 3 ------> API server
|-- Chunk 4 ------> API server
`-- ...
The server can adapt the chunk size based on the values being returned.
Large objects can therefore result in smaller chunks, while collections of small objects can be processed more efficiently.
The API server processes each chunk as it arrives and can release that chunk before receiving the next one.
How Kubernetes 1.37 Uses RangeStream
Kubernetes 1.37 adds the EtcdRangeStream feature gate.
It is Beta and enabled by default.
When the feature is active and the connected etcd server supports RangeStream, the API server can use streaming reads for large collections.
The main consumer is watch cache initialization.
The process becomes:
etcd
|
| RangeStream
|
+-- Chunk 1
| |
| v
| Decode
|
+-- Chunk 2
| |
| v
| Decode
|
+-- Chunk 3
| |
| v
| Decode
|
`-- ...
The API server does not need to assemble the entire collection in memory before beginning to process it.
This is the central memory improvement.
Watch Cache Initialization
The API server maintains an in-memory watch cache for many Kubernetes resources.
The cache allows normal list and watch operations to be served efficiently without querying etcd for every request.
When the API server starts or a watch cache needs to be rebuilt, it has to populate that cache from etcd.
For a small cluster, this is usually uninteresting.
For a large cluster, it can be expensive.
For example:
Cluster
|
+-- 5,000 nodes
|
+-- Hundreds of thousands of Pods
|
+-- Large CRDs
|
`-- Other high-volume resources
The API server may need to read and decode a very large amount of data before the cache is ready.
RangeStream allows the API server to start processing the result while etcd is still streaming the remaining data.
Memory Behavior Before and After
The difference can be simplified like this.
Traditional Range
Memory
^
|
| ###########
| ###########
| ###########
| ###########
|________###########
|
+----------------------> Time
large peak
The complete response has to exist before processing can proceed.
RangeStream
Memory
^
|
| ####
| #### ####
| #### #### ####
|_____##_##___####____####____
|
+-----------------------------> Time
chunks processed incrementally
The goal is not to make memory consumption zero.
The API server still needs memory for decoded objects and its watch cache.
The improvement is that temporary buffers for the large etcd response do not have to grow into one large payload.
RangeStream Also Helps etcd Memory
The memory improvement is not limited to kube-apiserver.
With the traditional unary Range RPC, etcd builds the result before sending it.
That means etcd can temporarily hold:
Key-value results
Serialized response data
gRPC buffers
Other processing structures
With RangeStream, etcd can produce smaller chunks and release data as the stream progresses.
This is especially useful because etcd is also responsible for maintaining the cluster's persistent state.
A memory spike in etcd can affect more than the request that caused it.
What Happens With Older etcd Versions?
Kubernetes 1.37 requires etcd 3.7 or later to actually use RangeStream.
The API server checks whether the etcd server supports the new RPC.
If the server responds with Unimplemented, the API server falls back to the existing paginated Range behavior.
That means an API server can remain operational when paired with an older etcd version, although it will not receive the RangeStream memory benefits.
The behavior can be represented as:
kube-apiserver
|
v
Does etcd support RangeStream?
|
+-- Yes --> Stream results
|
`-- No ---> Use paginated Range
This fallback is useful during controlled upgrades.
How to Verify RangeStream Is Being Used
Kubernetes exposes etcd request metrics that identify streamed reads.
The relevant operation label is:
operation="listStream"
For example:
etcd_request_duration_seconds_count{operation="listStream"}
A non-zero count indicates that the API server is using RangeStream.
If the count remains zero, investigate the etcd version and feature-gate configuration.
The most common reason is that the API server is connected to an etcd version that does not implement the required RPC.
Feature Gate Configuration
EtcdRangeStream is enabled by default in Kubernetes 1.37.
If you need to disable it temporarily:
--feature-gates=EtcdRangeStream=false
Disabling the feature returns the API server to its previous paginated Range behavior.
This can be useful for troubleshooting or when an etcd-compatible backend or proxy does not correctly handle the streaming RPC.
If you operate an etcd-compatible backend rather than standard etcd, test the behavior before enabling the feature across the cluster.
Direct List Requests Also Benefit
Watch cache initialization is the main use case, but it is not the only one.
The API server can also use RangeStream when it needs to read a collection directly from etcd.
This matters when a list request cannot be served through the normal watch-cache path.
The general flow is:
Client
|
v
kube-apiserver
|
+-- Watch cache available?
| |
| `-- Yes --> Serve from cache
|
`-- No
|
v
etcd RangeStream
|
v
Process chunks
The benefit is therefore broader than just startup behavior.
Large Objects Matter More Than Object Count
One of the most useful operational lessons from RangeStream is that object count alone is a poor way to estimate memory pressure.
Suppose a resource contains:
100,000 small objects
Join the conversation! Your thoughts help the community grow.