diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/PropagationScenariosBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/PropagationScenariosBenchmark.java
new file mode 100644
index 00000000000..746357ebadc
--- /dev/null
+++ b/dd-trace-core/src/jmh/java/datadog/trace/core/PropagationScenariosBenchmark.java
@@ -0,0 +1,241 @@
+package datadog.trace.core;
+
+import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT;
+import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE;
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+
+import datadog.context.Context;
+import datadog.context.propagation.CarrierSetter;
+import datadog.context.propagation.CarrierVisitor;
+import datadog.context.propagation.Propagators;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
+import datadog.trace.common.sampling.Sampler;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Per-request tracing cost of the standard propagation scenarios, with a default-configured tracer
+ * (default propagation styles: Datadog, W3C trace context and baggage). It puts a cost on the
+ * per-trace sampling and propagation bookkeeping, such as the OpenTelemetry {@code ot} tracestate
+ * member that v1.67.0 started generating, as a service actually sees it.
+ *
+ *
This is a scenario benchmark: each arm is a whole request as a service handles it,
+ * driven through the tracer's public API, rather than one method in isolation. A change to any
+ * piece shows up as what it costs a real request, and the result doesn't depend on which internals
+ * the benchmark happens to call. The scenarios:
+ *
+ *
+ * - {@link #headNoPropagation}: a root service span that makes the sampling decision (at
+ * finish) and calls nothing downstream.
+ *
- {@link #headPropagation}: a root service span plus an outbound client span that injects, so
+ * the decision is made at injection and written into the headers.
+ *
- {@link #leaf}: extracts an upstream decision and calls nothing downstream.
+ *
- {@link #intermediate}: extracts an upstream decision and relays it through an outbound
+ * client span.
+ *
+ *
+ * {@code upstream} selects what the extracting scenarios receive: {@code sameVersion} is what
+ * this tracer itself injects (a fleet on one version), and {@code withOtelMember} is a fixed
+ * current format carrying an {@code ot} member (an upgraded upstream, whatever this tracer's
+ * version). Upstream headers cycle through 1024 distinct trace ids so caches keyed by header
+ * content see realistic variety.
+ *
+ *
Read the allocation columns. Run with {@code -prof gc}; {@code gc.alloc.rate.norm}
+ * (B/op) is the primary signal. Throughput is directional only.
+ *
+ *
Drift-stable so it can be built onto past release tags (e.g. v1.66.0, the last release
+ * before the {@code ot} member was generated). It uses only the {@code datadog.context} propagation
+ * API, {@link CoreTracer} builder/span API, {@link Sampler.Builder#forConfig} and {@link
+ * DropWriter}, all unchanged from v1.66.0 to master. Keep it inside that surface.
+ *
+ *
+ * ./gradlew :dd-trace-core:jmhJar
+ * java -jar dd-trace-core/build/libs/dd-trace-core-*-jmh.jar PropagationScenariosBenchmark -prof gc
+ *
+ */
+@State(Scope.Benchmark)
+@Warmup(iterations = 5)
+@Measurement(iterations = 5)
+@BenchmarkMode(Mode.Throughput)
+@Threads(8)
+@OutputTimeUnit(MICROSECONDS)
+@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn")
+public class PropagationScenariosBenchmark {
+ private static final String INSTRUMENTATION_NAME = "bench";
+ private static final String SERVER_OPERATION_NAME = "servlet.request";
+ private static final String CLIENT_OPERATION_NAME = "http.request";
+ private static final int UPSTREAM_TRACE_COUNT = 1024;
+ // 128-bit trace ids carry their creation time in the top 32 bits, the rest of the high half zero
+ private static final long TRACE_ID_EPOCH_SECONDS = 1_760_000_000L;
+
+ /** Injects into the benchmark's blackhole, so header values are consumed without a map. */
+ private static final CarrierSetter BLACKHOLE_SETTER =
+ (blackhole, key, value) -> blackhole.consume(value);
+
+ private static final CarrierVisitor