diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs
--- a/crates/uv-bench/benches/uv.rs
+++ b/crates/uv-bench/benches/uv.rs
@@ -511,6 +511,8 @@
             .unwrap();
     }
 
+    uv_pep508::marker::benchmark_interner_stats();
+
     // No matter how long the benchmarks run, never do fresh network requests
     let client = RegistryClientBuilder::new(
         BaseClientBuilder::default().connectivity(Connectivity::Offline),
@@ -529,6 +531,7 @@
                 universal,
             ))
             .unwrap();
+        uv_pep508::marker::benchmark_interner_stats();
     }
 }
 
diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs
--- a/crates/uv-pep508/src/marker/algebra.rs
+++ b/crates/uv-pep508/src/marker/algebra.rs
@@ -48,7 +48,9 @@
 use std::cmp::Ordering;
 use std::fmt;
 use std::hash::BuildHasher;
+use std::mem::size_of;
 use std::ops::Bound;
+use std::sync::atomic::AtomicBool;
 use std::sync::{LazyLock, Mutex, MutexGuard};
 
 use arcstr::ArcStr;
@@ -75,6 +77,45 @@
 
 /// The global node interner.
 pub(crate) static INTERNER: LazyLock<Interner> = LazyLock::new(Interner::default);
+
+pub fn benchmark_interner_stats() {
+    let interner = &*INTERNER;
+    let state = interner.state.lock().unwrap();
+    let count = interner.shared.nodes.count();
+    let capacity = if count == 0 {
+        0
+    } else {
+        (count + 32).next_power_of_two() - 32
+    };
+    let node_bytes = capacity * size_of::<(AtomicBool, Node)>();
+    let edge_bytes: usize = interner
+        .shared
+        .nodes
+        .iter()
+        .map(|(_, node)| match &node.children {
+            Edges::Version { edges } => edges.len() * size_of::<(Ranges<Version>, NodeId)>(),
+            Edges::String { edges } => edges.len() * size_of::<(Ranges<ArcStr>, NodeId)>(),
+            Edges::Boolean { .. } => 0,
+        })
+        .sum();
+    let cache_capacity = state.cache.capacity();
+    let cache_bytes = if cache_capacity == 0 {
+        0
+    } else {
+        let buckets = (cache_capacity * 8 / 7).next_power_of_two().max(4);
+        buckets * (size_of::<((NodeId, NodeId), NodeId)>() + 1) + 16
+    };
+    // Excludes indirect string/version allocations and allocator overhead.
+    let estimated_bytes = node_bytes
+        + edge_bytes
+        + state.unique.allocation_size()
+        + cache_bytes
+        + size_of::<Interner>();
+    eprintln!(
+        "MARKER_STATS nodes={count} node_size={} estimated_bytes={estimated_bytes}",
+        size_of::<Node>()
+    );
+}
 
 /// An interner for decision nodes.
 ///
diff --git a/crates/uv-pep508/src/marker/mod.rs b/crates/uv-pep508/src/marker/mod.rs
--- a/crates/uv-pep508/src/marker/mod.rs
+++ b/crates/uv-pep508/src/marker/mod.rs
@@ -16,6 +16,7 @@
 mod simplify;
 mod tree;
 
+pub use algebra::benchmark_interner_stats;
 pub use environment::{MarkerEnvironment, MarkerEnvironmentBuilder};
 pub use lowering::{
     CanonicalMarkerValueExtra, CanonicalMarkerValueString, CanonicalMarkerValueVersion,
