Skip to main content
October 13, 2026

Ensuring Similar Computational Resources to Stateful Workloads Running on a Diverse Server Fleet

Kristoffer Andersen
Close-up of a computer processor with gold contact points and small metallic components.
Share this article

Introduction

Engineering teams use Uber’s stateful orchestration platform, Odin, to run their workloads across on-premises and public clouds. There are around 5 million cores on Odin, but not all cores are created equal. Some CPU cores are old, some are new. We even have both ARM and x86 hosts. This means that single-core performance may vary by up to a factor of almost 2. Therefore, the CPU usage of a workload is dependent on which specific hosts it’s running on, not just the load patterns of the workload. As we have a highly heterogenous fleet, the result is that we often overallocate workloads if they’re running on fast CPUs and in some cases under allocate them on slow hardware. The first is an efficiency concern, and the latter is a reliability concern.

This blog covers how we modeled this difference and formalized it into NCUs (normalized compute units). This allows us to rightsize workloads on a heterogeneous fleet of servers at a global scale and give similar compute power to each workload independent of differences in host performance. This work is built on top of the Odin CPU model, which has been described in a previous blog.

Server Performance

Odin runs with a mix of on-premises data centers and in the public cloud. This means that we cover many generations of different hardware. On top of this, we use x86 and ARM-based hardware. ARM is a recent introduction to the fleet. 

‌This means that single-threaded performance varies by up to almost 100% between the fastest new x86 core and the slowest old core. Even among new servers, there’s a significant difference between the performance of an ARM core and an x86 core. 

Figure 1 shows a use case running on two types of x86 servers in the same data center. The upper blue line is for the slower old-generation hardware compared to ‌the newer generation hardware (green line). The CPU utilization of the old hardware is roughly 23% above the new hardware.

CPU utilization trends over time for slow and fast servers, with slow servers showing higher usage.

Figure 1: CPU utilization for 2 server types running the same workloads.

As a result, you can run the new server type at around an 18% lower CPU allocation and obtain the same CPU utilization. To provision capacity for the worst case, we allocate resources based on the hottest workload. This is generally the workloads on the slowest hardware. Being able to reduce allocations on ‌fast hardware is a significant savings possibility when running a fleet of millions of CPU cores.

Another effect of ‌uneven CPU load is that there might be reliability issues when workloads are moved from fast to slower hardware. Consider a use case with a clear leader-follower load pattern. This is shown in Figure 2. The CPU usage on the leader workload is more than 10 times the usage of the follower workloads. 

CPU usage spikes when leader moves from fast to slow server, shown by a sharp increase in the orange line.

Figure 2: CPU usage in a cluster with 3 workloads where the leader workload is running with ‌more than 10 times the load of the follower workloads.

We need to provision capacity based on the leader workload for all workloads in the cluster. This is because at any moment in time, one of the followers might become the leader. In cases where the leader workload is running on a fast server, ‌CPU usage is comparatively low. If the leader workload is now moved to a slower server type, then the CPU usage on the slow hardware will be much higher compared to the fast hardware. This can present a reliability concern. We address this by allocating resources based on performance instead of just cores. 

Ways to Adjust for Server Performance

One of the common ways to adjust for server performance is load balancing, which routes more traffic to ‌ servers with a small load and less to servers with a high load. The routing strategy can be based on static weights like performance factors or be based on dynamic data like latencies or CPU utilization. This strategy is used for stateless workloads where a request can go to any one instance of a particular service. The same isn’t the case for stateful workloads. Here, a request has to go to one of the servers that actually holds the queried data. On our stateless fleet, we have a global traffic routing layer. Such a similar global setup doesn’t exist in our stateful fleet to obtain an even CPU utilization.

On Odin, we’ve been running vertical CPU scaling for years. We use CPU sets to guarantee resources for workloads  and shared resources to perform some degree of over allocation. We call it dedicated and shared resources, respectively. The dedicated resources are integer cores and the latter is a decimal shared allocation, which translates into a shared pool on the hosts that all workloads have access to. We use CPU shares to split resources between workloads in case of contention. 

The CPU target for a cluster (a group of workloads on Odin like a data shard) is based on the CPU usage of all the nodes in the cluster. For a leader-follower case, we use the max load across the nodes in the cluster. Historically, we’ve been using base CPU usage from the nodes. However, as shown in Figure 2, this is like comparing apples to oranges. 2 cores of CPU usage on one server type might be equal to 3 cores of usage on a different server from a performance perspective.

Cluster workloads' CPU usage normalized and allocated by odin-scheduler for new workload deployment.

Figure 3: Schematic drawing for the setup for calculating and applying CPU resource targets based on normalized compute units.

In the current setup, we therefore convert all the CPU usage metrics into normalized units by multiplying the host performance factor. In our setup, we use a base 100 conversion factor based on an older server type. The fastest current servers have around a factor of 190. These performance factors are determined during Uber’s initial server qualification process and updated based on CPU usage data from production workloads. Based on the normalized values, we calculate an allocation target for the cluster in normalized units. This number is the required compute performance for workloads in the cluster. 

For workload resource updates and during workload creation, we then take this normalized target and convert it to physical cores for the specific host it is scheduled on. This results in different core allocations across workloads in the cluster but makes ‌CPU utilization more even. Now the low-utilized workloads due to fast hardware are scaled down and start to run hotter. 

Server type

Performance factor

Performance ratio (base ARM)

Cloud—newest generation ARM

127

1

On premise—generation 20

135

1.06

On premise—generation 21

151

1.19

Cloud—newest generation x86

196

1.54

Table 1: Performance factors for 4 server types running for Apache Kafka®.

Four colored lines track CPU cores per workload over time, with one line dropping sharply after late April.

Figure 4: Average CPU allocation per workload for a Kafka use case running on 4 different server types, where the blue line is for the fastest hardware.

Figure 4 shows how ‌CPU allocations on a workload level started to diverge once we rolled out the system. The workloads on fast hardware are now allocated close to 48 CPU cores, and the workloads on slower hardware is allocated more than 70 cores per workload. The downward trend off all the workloads isn’t an effect of ‌ performance-aware allocation, however. This is due to overall load changes. 

Performance-Aware Scheduling in Action

We’ve incrementally been rolling out the system to all of the automatically scaled workloads on Odin. This is roughly 1.4 million CPU cores. Early in the rollout, it became clear that we couldn’t fully rely on ‌performance factors from synthetic benchmarks. We needed to adjust the factors to match the actual performance seen in production. We also quickly saw that the different technologies had significantly different performance numbers per server type. We have therefore set up a system to continuously monitor ‌CPU usage across server types and estimate the performance factors on a technology level. This system is running daily, and we’re seeing significantly different performance factors for Apache Cassandra® and Kafka.

As mentioned initially, the goal was two-fold. We wanted to improve ‌reliability and reduce allocations by equalizing ‌CPU utilization across server types. As shown above, for Kafka, the system changes the workload allocations significantly. This results in a change, as in the CPU utilization as shown in Figure 5. The lower dark blue line is the fastest hardware and the upper blue line is the slower old hardware.

CPU utilization trends over time for slow and fast servers, with slow servers showing higher usage.

Figure 5: CPU utilization across 5 different server types for Kafka workloads during the NCU rollout.

The CPU utilization initially spanned around 8 percentage points, with the hottest running a 50% higher CPU utilization than the coldest. With the rollout, we’ve increased the CPU utilization of the fastest hardware. ‌CPU utilization now only spans 3 percentage points, ‌and the hottest has ‌only 10% higher CPU utilization than the slowest. Across Kafka, we’ve reduced the total cores allocated by around 8% with this setup. Furthermore, this system is mainly reducing allocations on the fast servers where the cores are most expensive. In other words, from a cost perspective, the savings are even greater. 

Figure 5 also shows that we aren’t fully there yet. However, it’ll be difficult to collapse the utilization lines fully. Part of this can be due to load variations across workloads, hot shards, and different server types mixes for the technologies. We’ll continue to monitor and tune the performance factors of the servers to obtain the most equal utilization across server types. 

Whereas the above only focuses on the efficiency improvements of this project, we also have cases where ‌ reliability has improved. There was, in particular, a case where a leader-follower-based use case saw degradations whenever the workloads weren’t placed on the fastest hardware. With the new setup, we can express minimum allocations in terms of both a normalized value as well as correcting the forecast based on the hardware. After the rollout, we haven’t had any reports with this issue.

What’s Next

Until now, Uber’s CPU allocation model has been centered around CPU cores. However, with the above changes, Odin moved up an abstraction layer and now focuses on compute power or performance instead. This is a large conceptual change from the focus on cores. Uber runs its own cloud with a large variety of server types, and where workloads are scheduled across the whole fleet to improve resource utilization and reliability (like a geographical spread of workloads in different data centers). So, we don’t want our internal customers to hand-pick particular server types. This should be abstracted away from the customer, who just needs sufficient resources to run their workload. We consider it a platform’s responsibility to ensure that workloads are given this and at the same time not more than is needed. Performance-aware scheduling makes this more efficient and reliable. 

Conclusion

The performance of different server types is very important when running stateful workloads across a diverse fleet. With the described system, we’re adjusting CPU allocations based on ‌server performance and thereby obtaining a more evenly distributed CPU utilization across different server types. This ensures a more efficient and reliable platform. 

Acknowledgments

This work was carried out in collaboration with multiple teams. In particular, we want to thank the Odin Scheduler team and the Fleet Performance team. Most of the work was carried out by former Senior Software Engineer Ulfat Abjabborov. 

Cover Photo Attribution: “Intel Celeron CPU” by Uwe Hermann Is covered by CC BY-SA 2.0.  

Apache®, Apache Kafka®, Apache Cassandra®, and the star logo are either registered trademarks or trademarks of the Apache Software Foundation in the United States and/or other countries. No endorsement by The Apache Software Foundation is implied by the use of these marks.

Written by

Kristoffer Andersen

Kristoffer Andersen is a software engineer on the Uber Eats team in Aarhus. He is working with the current and future catalog system and has in particular focused on improving the image pipeline and the observability.

Category
Related Articles
1 article
AI / ML
Backend
Engineering
October 1, 2026