diff --git a/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java
index 142f8da3c..aa9373c24 100644
--- a/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java
+++ b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java
@@ -158,6 +158,8 @@ public void failed(Exception ex) {
     @Override
     public void cancelled() {
       HttpResponse httpResponse = getResponse(httpContext);
+      Span span = Span.fromContext(context);
+      span.getSpanContext().getTraceState();
       ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
       delegate.cancelled();
     }
diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java
index c7343939a..8dc1de586 100644
--- a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java
+++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet31NoWrappingInstrumentation.java
@@ -56,6 +56,8 @@
 import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair;
 import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair;
 import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate.CaptureMode;
 import org.hypertrace.agent.filter.FilterRegistry;
 
 public class Servlet31NoWrappingInstrumentation implements TypeInstrumentation {
@@ -105,7 +107,9 @@ public static boolean start(
       AgentConfig agentConfig = HypertraceConfig.get();
       String contentType = httpRequest.getContentType();
       if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue()
-          && ContentTypeUtils.shouldCapture(contentType)) {
+          && ContentTypeUtils.shouldCapture(contentType)
+          && HypertraceTracestate.getCaptureMode(currentSpan.getSpanContext().getTraceState())
+              == CaptureMode.ALL) {
         // The HttpServletRequest instrumentation uses this to
         // enable the instrumentation
         InstrumentationContext.get(HttpServletRequest.class, SpanAndObjectPair.class)
@@ -123,7 +127,9 @@ public static boolean start(
         AttributeKey<String> attributeKey =
             HypertraceSemanticAttributes.httpRequestHeader(headerName);
 
-        if (HypertraceConfig.get().getDataCapture().getHttpHeaders().getRequest().getValue()) {
+        if (HypertraceConfig.get().getDataCapture().getHttpHeaders().getRequest().getValue()
+            && HypertraceTracestate.getCaptureMode(currentSpan.getSpanContext().getTraceState())
+                == CaptureMode.ALL) {
           currentSpan.setAttribute(attributeKey, headerValue);
         }
         headers.put(attributeKey.getKey(), headerValue);
@@ -195,7 +201,9 @@ public static void exit(
       }
 
       if (!request.isAsyncStarted() && responseHandled.compareAndSet(false, true)) {
-        if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) {
+        if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()
+            && HypertraceTracestate.getCaptureMode(currentSpan.getSpanContext().getTraceState())
+                == CaptureMode.ALL) {
           for (String headerName : httpResponse.getHeaderNames()) {
             String headerValue = httpResponse.getHeader(headerName);
             currentSpan.setAttribute(
@@ -216,7 +224,9 @@ public static void exit(
 
         // remove request body buffers from context stores, otherwise they might get reused
         if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue()
-            && ContentTypeUtils.shouldCapture(httpRequest.getContentType())) {
+            && ContentTypeUtils.shouldCapture(httpRequest.getContentType())
+            && HypertraceTracestate.getCaptureMode(currentSpan.getSpanContext().getTraceState())
+                == CaptureMode.ALL) {
           Utils.resetRequestBodyBuffers(
               httpRequest, requestContextStore, inputStreamContextStore, readerContextStore);
         }
diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java
index 1a0101092..a4b7bb4f7 100644
--- a/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java
+++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Utils.java
@@ -32,6 +32,8 @@
 import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter;
 import org.hypertrace.agent.core.instrumentation.buffer.ByteBufferSpanPair;
 import org.hypertrace.agent.core.instrumentation.buffer.CharBufferSpanPair;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate.CaptureMode;
 
 public class Utils {
 
@@ -65,9 +67,12 @@ public static void captureResponseBody(
       BoundedByteArrayOutputStream buffer = streamContextStore.get(servletOutputStream);
       if (buffer != null) {
         try {
-          span.setAttribute(
-              HypertraceSemanticAttributes.HTTP_RESPONSE_BODY,
-              buffer.toStringWithSuppliedCharset());
+          if (HypertraceTracestate.getCaptureMode(span.getSpanContext().getTraceState())
+              == CaptureMode.ALL) {
+            span.setAttribute(
+                HypertraceSemanticAttributes.HTTP_RESPONSE_BODY,
+                buffer.toStringWithSuppliedCharset());
+          }
         } catch (UnsupportedEncodingException e) {
           // should not happen
         }
@@ -77,7 +82,10 @@ public static void captureResponseBody(
       PrintWriter printWriter = (PrintWriter) responseStreamWriterHolder.getAssociatedObject();
       BoundedCharArrayWriter buffer = writerContextStore.get(printWriter);
       if (buffer != null) {
-        span.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, buffer.toString());
+        if (HypertraceTracestate.getCaptureMode(span.getSpanContext().getTraceState())
+            == CaptureMode.ALL) {
+          span.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, buffer.toString());
+        }
         writerContextStore.put(printWriter, null);
       }
     }
diff --git a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java
index 45a92e42a..1e494b4c2 100644
--- a/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java
+++ b/instrumentation/servlet/servlet-3.0-no-wrapping/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/servlet/v3_0/nowrapping/Servlet30NoWrappingInstrumentationTest.java
@@ -60,6 +60,7 @@ public static void startServer() throws Exception {
     handler.addFilter(WrappingFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
 
     handler.addServlet(GetHello.class, "/hello");
+    handler.addServlet(EchoStream_single_byte.class, "/default_capture");
     handler.addServlet(EchoStream_single_byte.class, "/echo_stream_single_byte");
     handler.addServlet(EchoStream_arr.class, "/echo_stream_arr");
     handler.addServlet(EchoStream_arr_offset.class, "/echo_stream_arr_offset");
@@ -301,4 +302,82 @@ public void postJson(String url) throws Exception {
         TestServlets.RESPONSE_BODY,
         spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY));
   }
+
+  @Test
+  public void postTraceStateNoCapture() throws Exception {
+    Request request =
+        new Request.Builder()
+            .url(String.format("http://localhost:%d/echo_stream_single_byte", serverPort))
+            .post(RequestBody.create(REQUEST_BODY, MediaType.get("application/json")))
+            .header(REQUEST_HEADER, REQUEST_HEADER_VALUE)
+            .header("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b6-01")
+            .header("tracestate", "hypertrace=cap:0")
+            .build();
+    try (Response response = httpClient.newCall(request).execute()) {
+      Assertions.assertEquals(200, response.code());
+      Assertions.assertEquals(TestServlets.RESPONSE_BODY, response.body().string());
+    }
+
+    List<List<SpanData>> traces = TEST_WRITER.getTraces();
+
+    Assertions.assertEquals(1, traces.size());
+    List<SpanData> spans = traces.get(0);
+    Assertions.assertEquals(1, spans.size());
+    SpanData spanData = spans.get(0);
+    Assertions.assertNull(
+        spanData
+            .getAttributes()
+            .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER)));
+    Assertions.assertNull(
+        spanData
+            .getAttributes()
+            .get(HypertraceSemanticAttributes.httpResponseHeader(TestServlets.RESPONSE_HEADER)));
+    Assertions.assertNull(
+        spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY));
+    Assertions.assertNull(
+        spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY));
+  }
+
+  // TODO
+  //  @Test
+  //  public void postTraceStateNoCapture2() throws Exception {
+  //    Request request =
+  //        new Request.Builder()
+  //            .url(String.format("http://localhost:%d/default_capture", serverPort))
+  //            .post(RequestBody.create(REQUEST_BODY, MediaType.get("application/json")))
+  //            .header(REQUEST_HEADER, REQUEST_HEADER_VALUE)
+  //            .build();
+  //    try (Response response = httpClient.newCall(request).execute()) {
+  //      Assertions.assertEquals(200, response.code());
+  //      Assertions.assertEquals(TestServlets.RESPONSE_BODY, response.body().string());
+  //    }
+  //
+  //    // the second request should not have payloads because the sampler samples only 1 trace per
+  // second
+  //    // and we assume the two requests will be executed in one second
+  //    try (Response response = httpClient.newCall(request).execute()) {
+  //      Assertions.assertEquals(200, response.code());
+  //      Assertions.assertEquals(TestServlets.RESPONSE_BODY, response.body().string());
+  //    }
+  //
+  //    List<List<SpanData>> traces = TEST_WRITER.getTraces();
+  //
+  //    Assertions.assertEquals(1, traces.size());
+  //    List<SpanData> spans = traces.get(0);
+  //    Assertions.assertEquals(1, spans.size());
+  //    SpanData spanData = spans.get(0);
+  //    Assertions.assertNull(
+  //        spanData
+  //            .getAttributes()
+  //            .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER)));
+  //    Assertions.assertNull(
+  //        spanData
+  //            .getAttributes()
+  //
+  // .get(HypertraceSemanticAttributes.httpResponseHeader(TestServlets.RESPONSE_HEADER)));
+  //    Assertions.assertNull(
+  //        spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY));
+  //    Assertions.assertNull(
+  //        spanData.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY));
+  //  }
 }
diff --git a/javaagent-core/src/main/java/org/hypertrace/agent/core/propagation/HypertraceTracestate.java b/javaagent-core/src/main/java/org/hypertrace/agent/core/propagation/HypertraceTracestate.java
new file mode 100644
index 000000000..5ab63f690
--- /dev/null
+++ b/javaagent-core/src/main/java/org/hypertrace/agent/core/propagation/HypertraceTracestate.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.core.propagation;
+
+import io.opentelemetry.api.trace.TraceState;
+
+public class HypertraceTracestate {
+
+  private HypertraceTracestate() {}
+
+  static final String KEY = "hypertrace";
+
+  /**
+   * Create creates Hypertrace tracestate header
+   *
+   * @return Hypertrace tracestate header
+   */
+  public static TraceState create(TraceState traceState, CaptureMode captureMode) {
+    return traceState.toBuilder().put(KEY, String.format("cap:%d", captureMode.value)).build();
+  }
+
+  public static CaptureMode getCaptureMode(TraceState traceState) {
+    String htTraceState = traceState.get(KEY);
+    if (htTraceState == null || htTraceState.isEmpty()) {
+      return CaptureMode.UNDEFINED;
+    }
+    if (htTraceState.startsWith("cap:")) {
+      try {
+        String pMode = htTraceState.substring(htTraceState.indexOf(":") + 1);
+        int i = Integer.parseInt(pMode);
+        if (i == CaptureMode.ALL.value) {
+          return CaptureMode.ALL;
+        } else if (i == CaptureMode.DEFAULT.value) {
+          return CaptureMode.DEFAULT;
+        } else {
+          return CaptureMode.UNDEFINED;
+        }
+      } catch (Throwable ex) {
+      }
+    } else {
+      return CaptureMode.UNDEFINED;
+    }
+    return CaptureMode.ALL;
+  }
+
+  public enum CaptureMode {
+    UNDEFINED(-1),
+    /**
+     * Headers and payloads are not collected. Except allowed headers (content-type and user-agent).
+     */
+    DEFAULT(0),
+    /** All data is collected - payloads and headers. */
+    ALL(1);
+
+    private final int value;
+
+    CaptureMode(int value) {
+      this.value = value;
+    }
+  }
+}
diff --git a/javaagent-core/src/test/java/org/hypertrace/agent/core/propagation/HypertraceTracestateTest.java b/javaagent-core/src/test/java/org/hypertrace/agent/core/propagation/HypertraceTracestateTest.java
new file mode 100644
index 000000000..2fe6d383b
--- /dev/null
+++ b/javaagent-core/src/test/java/org/hypertrace/agent/core/propagation/HypertraceTracestateTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.core.propagation;
+
+import io.opentelemetry.api.trace.TraceState;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate.CaptureMode;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class HypertraceTracestateTest {
+
+  @Test
+  public void create() {
+    Assertions.assertEquals(
+        "cap:1",
+        HypertraceTracestate.create(TraceState.getDefault(), CaptureMode.ALL)
+            .get(HypertraceTracestate.KEY));
+    Assertions.assertEquals(
+        "cap:0",
+        HypertraceTracestate.create(TraceState.getDefault(), CaptureMode.DEFAULT)
+            .get(HypertraceTracestate.KEY));
+  }
+
+  @Test
+  public void parseProtectionMode_advanced() {
+    TraceState traceState =
+        TraceState.getDefault().toBuilder().put(HypertraceTracestate.KEY, "cap:1").build();
+    CaptureMode captureMode = HypertraceTracestate.getCaptureMode(traceState);
+    Assertions.assertEquals(CaptureMode.ALL, captureMode);
+  }
+
+  @Test
+  public void parseProtectionMode_core() {
+    TraceState traceState =
+        TraceState.getDefault().toBuilder().put(HypertraceTracestate.KEY, "cap:0").build();
+    CaptureMode captureMode = HypertraceTracestate.getCaptureMode(traceState);
+    Assertions.assertEquals(CaptureMode.DEFAULT, captureMode);
+  }
+
+  @Test
+  public void parseProtectionMode_unknown() {
+    TraceState traceState =
+        TraceState.getDefault().toBuilder().put(HypertraceTracestate.KEY, "cap:2").build();
+    CaptureMode captureMode = HypertraceTracestate.getCaptureMode(traceState);
+    Assertions.assertEquals(CaptureMode.UNDEFINED, captureMode);
+
+    traceState = TraceState.getDefault().toBuilder().put(HypertraceTracestate.KEY, "cap:2").build();
+    captureMode = HypertraceTracestate.getCaptureMode(traceState);
+    Assertions.assertEquals(CaptureMode.UNDEFINED, captureMode);
+
+    traceState = TraceState.getDefault().toBuilder().put(HypertraceTracestate.KEY, " ").build();
+    captureMode = HypertraceTracestate.getCaptureMode(traceState);
+    Assertions.assertEquals(CaptureMode.UNDEFINED, captureMode);
+  }
+}
diff --git a/otel-extensions/build.gradle.kts b/otel-extensions/build.gradle.kts
index 5680cd561..0fbbd3a00 100644
--- a/otel-extensions/build.gradle.kts
+++ b/otel-extensions/build.gradle.kts
@@ -6,9 +6,12 @@ val versions: Map<String, String> by extra
 
 dependencies {
     api(project(":filter-api"))
+    api(project(":javaagent-core"))
 
     compileOnly("io.opentelemetry:opentelemetry-sdk:${versions["opentelemetry"]}")
     compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions["opentelemetry"]}-alpha")
+    compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions["opentelemetry"]}-alpha")
+    implementation("io.opentelemetry:opentelemetry-sdk-common:${versions["opentelemetry"]}-alpha")
     implementation("io.opentelemetry:opentelemetry-semconv:${versions["opentelemetry"]}-alpha")
     implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-spi:${versions["opentelemetry_java_agent"]}")
 
@@ -19,4 +22,7 @@ dependencies {
 
     testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions["opentelemetry"]}-alpha")
     testImplementation("io.opentelemetry:opentelemetry-sdk:${versions["opentelemetry"]}")
+
+    // for copied RateLimiterTest
+    testImplementation("org.assertj:assertj-core:3.19.0")
 }
diff --git a/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/AdditionalCaptureSampler.java b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/AdditionalCaptureSampler.java
new file mode 100644
index 000000000..e8899295b
--- /dev/null
+++ b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/AdditionalCaptureSampler.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import io.opentelemetry.sdk.trace.samplers.Sampler;
+import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
+import io.opentelemetry.sdk.trace.samplers.SamplingResult;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate;
+import org.hypertrace.agent.core.propagation.HypertraceTracestate.CaptureMode;
+
+public class AdditionalCaptureSampler implements Sampler {
+
+  private static final HypertraceSamplingResult allSamplingResult =
+      new HypertraceSamplingResult(CaptureMode.ALL);
+  private static final HypertraceSamplingResult defaultSamplingResult =
+      new HypertraceSamplingResult(CaptureMode.DEFAULT);
+
+  private List<SampledEndpoint> sampledEndpoints;
+
+  public AdditionalCaptureSampler(List<String> urlPatterns) {
+    sampledEndpoints = new ArrayList<>(urlPatterns.size());
+    for (String pattern : urlPatterns) {
+      sampledEndpoints.add(
+          new SampledEndpoint(new RateLimitingSampler(1), Pattern.compile(pattern)));
+    }
+  }
+
+  @Override
+  public SamplingResult shouldSample(
+      Context parentContext,
+      String traceId,
+      String name,
+      SpanKind spanKind,
+      Attributes attributes,
+      List<LinkData> parentLinks) {
+
+    Span span = Span.fromContext(parentContext);
+    SpanContext spanContext = span.getSpanContext();
+
+    CaptureMode captureMode = HypertraceTracestate.getCaptureMode(spanContext.getTraceState());
+    if (captureMode == CaptureMode.ALL) {
+      // core mode already defer to the default sampler
+      return allSamplingResult;
+    } else if (captureMode == CaptureMode.DEFAULT) {
+      return defaultSamplingResult;
+    }
+
+    String urlAttr = attributes.get(SemanticAttributes.HTTP_URL);
+    if (urlAttr != null && !urlAttr.isEmpty()) {
+      String path;
+      try {
+        URL url = new URL(urlAttr);
+        path = url.getPath();
+      } catch (MalformedURLException e) {
+        return defaultSamplingResult;
+      }
+
+      if (path == null || path.isEmpty()) {
+        return allSamplingResult;
+      }
+
+      for (SampledEndpoint sampledEndpoint : this.sampledEndpoints) {
+        if (sampledEndpoint.pattern.matcher(path).matches()) {
+          SamplingResult samplingResult =
+              sampledEndpoint.sampler.shouldSample(
+                  parentContext, traceId, name, spanKind, attributes, parentLinks);
+          if (samplingResult.getDecision() == SamplingDecision.RECORD_AND_SAMPLE) {
+            // set the advanced mode - record all
+            return allSamplingResult;
+          } else {
+            // set the core mode
+            return defaultSamplingResult;
+          }
+        }
+      }
+    }
+    // default use the advanced mode
+    return allSamplingResult;
+  }
+
+  @Override
+  public String getDescription() {
+    return "Samples additional";
+  }
+
+  static class HypertraceSamplingResult implements SamplingResult {
+
+    final CaptureMode captureMode;
+
+    public HypertraceSamplingResult(CaptureMode captureMode) {
+      this.captureMode = captureMode;
+    }
+
+    @Override
+    public SamplingDecision getDecision() {
+      return SamplingDecision.RECORD_AND_SAMPLE;
+    }
+
+    @Override
+    public Attributes getAttributes() {
+      return Attributes.empty();
+    }
+
+    @Override
+    public TraceState getUpdatedTraceState(TraceState parentTraceState) {
+      return HypertraceTracestate.create(parentTraceState, captureMode);
+    }
+  }
+
+  static class SampledEndpoint {
+    private final RateLimitingSampler sampler;
+    private final Pattern pattern;
+
+    public SampledEndpoint(RateLimitingSampler sampler, Pattern pattern) {
+      this.sampler = sampler;
+      this.pattern = pattern;
+    }
+  }
+}
diff --git a/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/GlobToRegex.java b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/GlobToRegex.java
new file mode 100644
index 000000000..c4dbc414b
--- /dev/null
+++ b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/GlobToRegex.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+class GlobToRegex {
+  private GlobToRegex() {}
+
+  public static String convertGlobToRegex(String pattern) {
+    StringBuilder sb = new StringBuilder(pattern.length());
+    for (int i = 0; i < pattern.length(); i++) {
+      char ch = pattern.charAt(i);
+      if (ch == '*') {
+        if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '*') {
+          sb.append(".*");
+        } else {
+          sb.append("[^\\/]*(|\\/)+");
+        }
+      } else if (ch == '*' && i > 0 && pattern.charAt(i - 1) == '*') {
+        continue;
+      } else {
+        sb.append(ch);
+      }
+    }
+    return sb.toString();
+  }
+}
diff --git a/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/RateLimiter.java b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/RateLimiter.java
new file mode 100644
index 000000000..1e0ba928e
--- /dev/null
+++ b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/RateLimiter.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import io.opentelemetry.sdk.common.Clock;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * This class was taken from Jaeger java client.
+ * https://github.com/jaegertracing/jaeger-client-java/blob/master/jaeger-core/src/main/java/io/jaegertracing/internal/samplers/RateLimitingSampler.java
+ *
+ * <p>Variables have been renamed for clarity.
+ *
+ * <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public class RateLimiter {
+  private final Clock clock;
+  private final double creditsPerNanosecond;
+  private final long maxBalance; // max balance in nano ticks
+  private final AtomicLong currentBalance; // last op nano time less remaining balance
+
+  /**
+   * Create a new RateLimiter with the provided parameters.
+   *
+   * @param creditsPerSecond How many credits to accrue per second.
+   * @param maxBalance The maximum balance that the limiter can hold, which corresponds to the rate
+   *     that is being limited to.
+   * @param clock An implementation of the {@link Clock} interface.
+   */
+  public RateLimiter(double creditsPerSecond, double maxBalance, Clock clock) {
+    this.clock = clock;
+    this.creditsPerNanosecond = creditsPerSecond / 1.0e9;
+    this.maxBalance = (long) (maxBalance / creditsPerNanosecond);
+    this.currentBalance = new AtomicLong(clock.nanoTime() - this.maxBalance);
+  }
+
+  /**
+   * Check to see if the provided cost can be spent within the current limits. Will deduct the cost
+   * from the current balance if it can be spent.
+   */
+  public boolean trySpend(double itemCost) {
+    long cost = (long) (itemCost / creditsPerNanosecond);
+    long currentNanos;
+    long currentBalanceNanos;
+    long availableBalanceAfterWithdrawal;
+    do {
+      currentBalanceNanos = this.currentBalance.get();
+      currentNanos = clock.nanoTime();
+      long currentAvailableBalance = currentNanos - currentBalanceNanos;
+      if (currentAvailableBalance > maxBalance) {
+        currentAvailableBalance = maxBalance;
+      }
+      availableBalanceAfterWithdrawal = currentAvailableBalance - cost;
+      if (availableBalanceAfterWithdrawal < 0) {
+        return false;
+      }
+    } while (!this.currentBalance.compareAndSet(
+        currentBalanceNanos, currentNanos - availableBalanceAfterWithdrawal));
+    return true;
+  }
+}
diff --git a/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/RateLimitingSampler.java b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/RateLimitingSampler.java
new file mode 100644
index 000000000..537792676
--- /dev/null
+++ b/otel-extensions/src/main/java/org/hypertrace/agent/otel/extensions/sampler/RateLimitingSampler.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.internal.SystemClock;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import io.opentelemetry.sdk.trace.samplers.Sampler;
+import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
+import io.opentelemetry.sdk.trace.samplers.SamplingResult;
+import java.util.List;
+
+/**
+ * {@link RateLimitingSampler} sampler uses a leaky bucket rate limiter to ensure that traces are
+ * sampled with a certain constant rate.
+ */
+class RateLimitingSampler implements Sampler {
+  static final String TYPE = "ratelimiting";
+  static final AttributeKey<String> SAMPLER_TYPE = AttributeKey.stringKey("sampler.type");
+  static final AttributeKey<Double> SAMPLER_PARAM = AttributeKey.doubleKey("sampler.param");
+
+  private final double maxTracesPerSecond;
+  private final RateLimiter rateLimiter;
+  private final SamplingResult onSamplingResult;
+  private final SamplingResult offSamplingResult;
+
+  /**
+   * Creates rate limiting sampler.
+   *
+   * @param maxTracesPerSecond the maximum number of sampled traces per second.
+   */
+  RateLimitingSampler(int maxTracesPerSecond) {
+    this.maxTracesPerSecond = maxTracesPerSecond;
+    double maxBalance = maxTracesPerSecond < 1.0 ? 1.0 : maxTracesPerSecond;
+    this.rateLimiter = new RateLimiter(maxTracesPerSecond, maxBalance, SystemClock.getInstance());
+    Attributes attributes =
+        Attributes.of(SAMPLER_TYPE, TYPE, SAMPLER_PARAM, (double) maxTracesPerSecond);
+    this.onSamplingResult = SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE, attributes);
+    this.offSamplingResult = SamplingResult.create(SamplingDecision.DROP, attributes);
+  }
+
+  @Override
+  public SamplingResult shouldSample(
+      Context parentContext,
+      String traceId,
+      String name,
+      SpanKind spanKind,
+      Attributes attributes,
+      List<LinkData> parentLinks) {
+    return this.rateLimiter.trySpend(1.0) ? onSamplingResult : offSamplingResult;
+  }
+
+  @Override
+  public String getDescription() {
+    return String.format("RateLimitingSampler{%.2f}", maxTracesPerSecond);
+  }
+
+  @Override
+  public String toString() {
+    return getDescription();
+  }
+}
diff --git a/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/CaptureModeSamplerTest.java b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/CaptureModeSamplerTest.java
new file mode 100644
index 000000000..92295ab88
--- /dev/null
+++ b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/CaptureModeSamplerTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.samplers.SamplingResult;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class CaptureModeSamplerTest {
+
+  @Test
+  public void traceStateCore() {
+    AdditionalCaptureSampler modeSampler = new AdditionalCaptureSampler(Arrays.asList("/foo/bar"));
+
+    SamplingResult samplingResult =
+        modeSampler.shouldSample(
+            Context.root(),
+            "0000",
+            "GET",
+            SpanKind.SERVER,
+            Attributes.of(SemanticAttributes.HTTP_URL, "http://unicorn.foo/foo/bar"),
+            Collections.emptyList());
+
+    TraceState traceState = samplingResult.getUpdatedTraceState(TraceState.getDefault());
+    String hypertraceTraceState = traceState.get("hypertrace");
+    // the first one is sampled as advanced
+    Assertions.assertEquals("cap:1", hypertraceTraceState);
+
+    samplingResult =
+        modeSampler.shouldSample(
+            Context.root(),
+            "0000",
+            "GET",
+            SpanKind.SERVER,
+            Attributes.of(SemanticAttributes.HTTP_URL, "http://unicorn.foo/foo/bar"),
+            Collections.emptyList());
+
+    traceState = samplingResult.getUpdatedTraceState(TraceState.getDefault());
+    hypertraceTraceState = traceState.get("hypertrace");
+    // the second one is sampled as core
+    Assertions.assertEquals("cap:0", hypertraceTraceState);
+  }
+
+  @Test
+  public void traceStateAdvanced() {
+    AdditionalCaptureSampler modeSampler = new AdditionalCaptureSampler(Arrays.asList("/foo/bar"));
+
+    SamplingResult samplingResult =
+        modeSampler.shouldSample(
+            Context.root(),
+            "00000",
+            "GET",
+            SpanKind.SERVER,
+            Attributes.of(SemanticAttributes.HTTP_URL, "http://unicorn.foo/foo/baz"),
+            Collections.emptyList());
+
+    TraceState traceState = samplingResult.getUpdatedTraceState(TraceState.getDefault());
+    String hypertraceTraceState = traceState.get("hypertrace");
+    Assertions.assertEquals("cap:1", hypertraceTraceState);
+  }
+}
diff --git a/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/GlobToRegexTest.java b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/GlobToRegexTest.java
new file mode 100644
index 000000000..bee493906
--- /dev/null
+++ b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/GlobToRegexTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import static org.hypertrace.agent.otel.extensions.sampler.GlobToRegex.convertGlobToRegex;
+
+import java.util.regex.Pattern;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class GlobToRegexTest {
+
+  @Test
+  public void globbing() {
+    Pattern pattern = Pattern.compile(convertGlobToRegex("/foo/**"));
+    Assertions.assertEquals(false, pattern.matcher("/foe/bar/baz").matches());
+    Assertions.assertEquals(true, pattern.matcher("/foo/bar/baz").matches());
+    Assertions.assertEquals(true, pattern.matcher("/foo/bazar").matches());
+  }
+
+  @Test
+  public void singleStar() {
+    Pattern pattern = Pattern.compile(convertGlobToRegex("/foo/*"));
+    Assertions.assertEquals(false, pattern.matcher("/foe/bar/baz").matches());
+    Assertions.assertEquals(false, pattern.matcher("/foo/bar/baz").matches());
+    Assertions.assertEquals(true, pattern.matcher("/foo/bar/").matches());
+    Assertions.assertEquals(true, pattern.matcher("/foo/bazar").matches());
+  }
+}
diff --git a/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/RateLimiterTest.java b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/RateLimiterTest.java
new file mode 100644
index 000000000..e90e7bf67
--- /dev/null
+++ b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/RateLimiterTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import io.opentelemetry.sdk.internal.TestClock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+/**
+ * This class was taken from Jaeger java client.
+ * https://github.com/jaegertracing/jaeger-client-java/blob/master/jaeger-core/src/test/java/io/jaegertracing/internal/utils/RateLimiterTest.java
+ */
+class RateLimiterTest {
+
+  @Test
+  void testRateLimiterWholeNumber() {
+    TestClock clock = TestClock.create();
+    RateLimiter limiter = new RateLimiter(2.0, 2.0, clock);
+
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+    // move time 250ms forward, not enough credits to pay for 1.0 item
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(250));
+    assertThat(limiter.trySpend(1.0)).isFalse();
+
+    // move time 500ms forward, now enough credits to pay for 1.0 item
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(500));
+
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+
+    // move time 5s forward, enough to accumulate credits for 10 messages, but it should still be
+    // capped at 2
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(5000));
+
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+  }
+
+  @Test
+  void testRateLimiterSteadyRate() {
+    TestClock clock = TestClock.create();
+    RateLimiter limiter = new RateLimiter(5.0 / 60.0, 5.0, clock);
+    for (int i = 0; i < 100; i++) {
+      assertThat(limiter.trySpend(1.0)).isTrue();
+      clock.advanceNanos(TimeUnit.SECONDS.toNanos(20));
+    }
+  }
+
+  @Test
+  void cantWithdrawMoreThanMax() {
+    TestClock clock = TestClock.create();
+    RateLimiter limiter = new RateLimiter(1, 1.0, clock);
+    assertThat(limiter.trySpend(2)).isFalse();
+  }
+
+  @Test
+  void testRateLimiterLessThanOne() {
+    TestClock clock = TestClock.create();
+    RateLimiter limiter = new RateLimiter(0.5, 0.5, clock);
+
+    assertThat(limiter.trySpend(0.25)).isTrue();
+    assertThat(limiter.trySpend(0.25)).isTrue();
+    assertThat(limiter.trySpend(0.25)).isFalse();
+    // move time 250ms forward, not enough credits to pay for 1.0 item
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(250));
+    assertThat(limiter.trySpend(0.25)).isFalse();
+
+    // move time 500ms forward, now enough credits to pay for 1.0 item
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(500));
+
+    assertThat(limiter.trySpend(0.25)).isTrue();
+    assertThat(limiter.trySpend(0.25)).isFalse();
+
+    // move time 5s forward, enough to accumulate credits for 10 messages, but it should still be
+    // capped at 2
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(5000));
+
+    assertThat(limiter.trySpend(0.25)).isTrue();
+    assertThat(limiter.trySpend(0.25)).isTrue();
+    assertThat(limiter.trySpend(0.25)).isFalse();
+    assertThat(limiter.trySpend(0.25)).isFalse();
+    assertThat(limiter.trySpend(0.25)).isFalse();
+  }
+
+  @Test
+  void testRateLimiterMaxBalance() {
+    TestClock clock = TestClock.create();
+    RateLimiter limiter = new RateLimiter(0.1, 1.0, clock);
+
+    clock.advanceNanos(TimeUnit.MICROSECONDS.toNanos(100));
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+
+    // move time 20s forward, enough to accumulate credits for 2 messages, but it should still be
+    // capped at 1
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(20000));
+
+    assertThat(limiter.trySpend(1.0)).isTrue();
+    assertThat(limiter.trySpend(1.0)).isFalse();
+  }
+
+  /**
+   * Validates rate limiter behavior with {@link System#nanoTime()}-like (non-zero) initial nano
+   * ticks.
+   */
+  @Test
+  void testRateLimiterInitial() {
+    TestClock clock = TestClock.create();
+    RateLimiter limiter = new RateLimiter(1000, 100, clock);
+
+    assertThat(limiter.trySpend(100)).isTrue(); // consume initial (max) balance
+    assertThat(limiter.trySpend(1)).isFalse();
+
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(49)); // add 49 credits
+    assertThat(limiter.trySpend(50)).isFalse();
+
+    clock.advanceNanos(TimeUnit.MILLISECONDS.toNanos(1)); // add one credit
+    assertThat(limiter.trySpend(50)).isTrue(); // consume accrued balance
+    assertThat(limiter.trySpend(1)).isFalse();
+
+    clock.advanceNanos(
+        TimeUnit.MILLISECONDS.toNanos(1_000_000)); // add a lot of credits (max out balance)
+    assertThat(limiter.trySpend(1)).isTrue(); // take one credit
+
+    clock.advanceNanos(
+        TimeUnit.MILLISECONDS.toNanos(1_000_000)); // add a lot of credits (max out balance)
+    assertThat(limiter.trySpend(101)).isFalse(); // can't consume more than max balance
+    assertThat(limiter.trySpend(100)).isTrue(); // consume max balance
+    assertThat(limiter.trySpend(1)).isFalse();
+  }
+
+  /** Validates concurrent credit check correctness. */
+  @Test
+  void testRateLimiterConcurrency() throws InterruptedException, ExecutionException {
+    int numWorkers = 8;
+    ExecutorService executorService = Executors.newFixedThreadPool(numWorkers);
+    final int creditsPerWorker = 1000;
+    TestClock clock = TestClock.create();
+    final RateLimiter limiter = new RateLimiter(1, numWorkers * creditsPerWorker, clock);
+    final AtomicInteger count = new AtomicInteger();
+    List<Future<?>> futures = new ArrayList<>(numWorkers);
+    for (int w = 0; w < numWorkers; ++w) {
+      Future<?> future =
+          executorService.submit(
+              () -> {
+                for (int i = 0; i < creditsPerWorker * 2; ++i) {
+                  if (limiter.trySpend(1)) {
+                    count.getAndIncrement(); // count allowed operations
+                  }
+                }
+              });
+      futures.add(future);
+    }
+    for (Future<?> future : futures) {
+      future.get();
+    }
+    executorService.shutdown();
+    executorService.awaitTermination(1, TimeUnit.SECONDS);
+    assertThat(count.get())
+        .withFailMessage("Exactly the allocated number of credits must be consumed")
+        .isEqualTo(numWorkers * creditsPerWorker);
+    assertThat(limiter.trySpend(1)).isFalse();
+  }
+}
diff --git a/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/RateLimitingSamplerTest.java b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/RateLimitingSamplerTest.java
new file mode 100644
index 000000000..773d0e7e7
--- /dev/null
+++ b/otel-extensions/src/test/java/org/hypertrace/agent/otel/extensions/sampler/RateLimitingSamplerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hypertrace.agent.otel.extensions.sampler;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
+import io.opentelemetry.sdk.trace.samplers.SamplingResult;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+class RateLimitingSamplerTest {
+
+  private static final String SPAN_NAME = "MySpanName";
+  private static final SpanKind SPAN_KIND = SpanKind.INTERNAL;
+  private static final String TRACE_ID = "12345678876543211234567887654321";
+  private static final String PARENT_SPAN_ID = "8765432112345678";
+  private static final Context spanContext =
+      Context.root()
+          .with(
+              Span.wrap(
+                  SpanContext.create(
+                      TRACE_ID, PARENT_SPAN_ID, TraceFlags.getDefault(), TraceState.getDefault())));
+
+  @Test
+  void sampleOneTrace() {
+    RateLimitingSampler sampler = new RateLimitingSampler(1);
+    SamplingResult samplingResult =
+        sampler.shouldSample(
+            spanContext,
+            TRACE_ID,
+            SPAN_NAME,
+            SPAN_KIND,
+            Attributes.empty(),
+            Collections.emptyList());
+    assertThat(samplingResult.getDecision()).isEqualTo(SamplingDecision.RECORD_AND_SAMPLE);
+    assertThat(
+            sampler
+                .shouldSample(
+                    spanContext,
+                    TRACE_ID,
+                    SPAN_NAME,
+                    SPAN_KIND,
+                    Attributes.empty(),
+                    Collections.emptyList())
+                .getDecision())
+        .isEqualTo(SamplingDecision.DROP);
+    assertThat(samplingResult.getAttributes().size()).isEqualTo(2);
+    assertThat(samplingResult.getAttributes().get(RateLimitingSampler.SAMPLER_PARAM)).isEqualTo(1d);
+    assertThat(samplingResult.getAttributes().get(RateLimitingSampler.SAMPLER_TYPE))
+        .isEqualTo(RateLimitingSampler.TYPE);
+  }
+
+  @Test
+  void description() {
+    RateLimitingSampler sampler = new RateLimitingSampler(15);
+    assertThat(sampler.getDescription()).isEqualTo("RateLimitingSampler{15.00}");
+  }
+}
diff --git a/testing-common/src/main/java/org/hypertrace/agent/testing/TestOpenTelemetryInstaller.java b/testing-common/src/main/java/org/hypertrace/agent/testing/TestOpenTelemetryInstaller.java
index 34d1d5a2e..e593f2ec2 100644
--- a/testing-common/src/main/java/org/hypertrace/agent/testing/TestOpenTelemetryInstaller.java
+++ b/testing-common/src/main/java/org/hypertrace/agent/testing/TestOpenTelemetryInstaller.java
@@ -22,6 +22,8 @@
 import io.opentelemetry.sdk.OpenTelemetrySdk;
 import io.opentelemetry.sdk.trace.SdkTracerProvider;
 import io.opentelemetry.sdk.trace.SpanProcessor;
+import java.util.Arrays;
+import org.hypertrace.agent.otel.extensions.sampler.AdditionalCaptureSampler;
 
 public class TestOpenTelemetryInstaller extends OpenTelemetryInstaller {
 
@@ -34,7 +36,11 @@ public TestOpenTelemetryInstaller(SpanProcessor spanProcessor) {
   @Override
   public void beforeByteBuddyAgent() {
     OpenTelemetrySdk.builder()
-        .setTracerProvider(SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build())
+        .setTracerProvider(
+            SdkTracerProvider.builder()
+                .addSpanProcessor(spanProcessor)
+                .setSampler(new AdditionalCaptureSampler(Arrays.asList("/default_capture")))
+                .build())
         .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
         .buildAndRegisterGlobal();
   }
