How Go 1.24's Swiss Tables saved us hundreds of gigabytes (opens in new tab)
Go 1.24 initially caused a Go runtime regression that increased RSS across Datadog services, but some high-traffic workloads ultimately used substantially less memory. The reduction came from Go 1.24’s Swiss Tables map implementation, which made a large, mostly read-only routing cache more compact. Profiling also revealed opportunities to reduce memory further by removing redundant data from the cached values.
The Unexpected Memory Reduction
- Datadog observed roughly 500 MiB less live heap in the
shardRoutingCachemap after upgrading to Go 1.24. - With
GOGC=100, that translated to approximately 1 GiB less total memory usage. - Even after accounting for an expected 400 MiB RSS increase from the
mallocgcregression, the service achieved a net reduction of about 600 MiB. - The improvement was most visible in high-traffic environments because they contained larger routing caches.
The shardRoutingCache Data Structure
The cache maps routing keys to shard information:
map[string]ResponseEach
Responsecontains:ShardID int32ShardTypeRoutingKey stringLastModified *time.Time
The map is populated mainly during service startup by querying a database.
It is rarely modified afterward, making its memory layout and initial allocation particularly important.
The routing key is stored both as the map key and again inside the value, creating potential redundancy.
Estimating Memory per Entry
- On a 64-bit system, a map key’s string header occupies 16 bytes.
- The value requires approximately:
- 4 bytes for
ShardID - 8 bytes for
ShardType - 16 bytes for the
RoutingKeystring header - 8 bytes for the
LastModifiedpointer
- 4 bytes for
- The value totals 36 bytes before alignment, or roughly 40 bytes with padding.
- Including the key header, each key-value pair requires about 56 bytes, excluding the separately allocated string and
time.Timedata.
Go 1.23 Bucket-Based Maps
- Go 1.23 maps used hash tables organized into an array of buckets.
- The number of buckets was always a power of two, and each bucket contained eight slots.
- Reads and writes required scanning the slots in the selected bucket to find a matching key or an empty position.
- When a bucket filled, Go added linked overflow buckets, which increased memory usage and made lookups more expensive.
- Map growth occurred when the average load factor exceeded 13/16, or 6.5 of 8 slots.
- The map then allocated twice as many buckets.
- To avoid a large latency spike, growth was incremental: old and new bucket arrays coexisted while entries were gradually moved during subsequent writes.
Why Workload Shape Matters
- The routing cache is populated in a startup-heavy phase and then primarily read.
- Such a workload benefits from a compact map representation because it does not need frequent insertions or growth.
- Differences in cache size and traffic patterns explain why the memory improvement was significant in some environments but not uniform across the fleet.
Go 1.24’s Swiss Tables implementation can substantially reduce memory usage for large, stable maps, even when another runtime change causes RSS growth. Teams should profile real production heaps after Go upgrades and inspect large structs for duplicated strings, unnecessary pointers, and other avoidable per-entry overhead.