diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml
index 35aa3b151d..604b674bad 100644
--- a/.github/workflows/hermetic_library_generation.yaml
+++ b/.github/workflows/hermetic_library_generation.yaml
@@ -37,7 +37,7 @@ jobs:
       with:
         fetch-depth: 0
         token: ${{ secrets.CLOUD_JAVA_BOT_TOKEN }}
-    - uses: googleapis/sdk-platform-java/.github/scripts@v2.50.0
+    - uses: googleapis/sdk-platform-java/.github/scripts@v2.51.0
       if: env.SHOULD_RUN == 'true'
       with:
         base_ref: ${{ github.base_ref }}
diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml
index dde9dfaa0b..74fa1adf9f 100644
--- a/.github/workflows/unmanaged_dependency_check.yaml
+++ b/.github/workflows/unmanaged_dependency_check.yaml
@@ -14,6 +14,6 @@ jobs:
       shell: bash
       run: .kokoro/build.sh
     - name: Unmanaged dependency check
-      uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.40.0
+      uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.41.0
       with:
         bom-path: google-cloud-bigtable-bom/pom.xml
diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg
index f8f242fab9..f0caedc501 100644
--- a/.kokoro/presubmit/graalvm-native-17.cfg
+++ b/.kokoro/presubmit/graalvm-native-17.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.40.0"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.41.0"
 }
 
 env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg
index 29c96a6d30..203d5d7be0 100644
--- a/.kokoro/presubmit/graalvm-native.cfg
+++ b/.kokoro/presubmit/graalvm-native.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.40.0"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.41.0"
 }
 
 env_vars: {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 974ce8dd2d..d0e5f1d678 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## [2.50.0](https://github.com/googleapis/java-bigtable/compare/v2.49.0...v2.50.0) (2024-12-06)
+
+
+### Features
+
+* Add support for Row Affinity app profiles ([#2341](https://github.com/googleapis/java-bigtable/issues/2341)) ([cb4d60e](https://github.com/googleapis/java-bigtable/commit/cb4d60e8ce2079a270739ad91efb05cbb1ff74f8))
+
 ## [2.49.0](https://github.com/googleapis/java-bigtable/compare/v2.48.0...v2.49.0) (2024-12-03)
 
 
diff --git a/README.md b/README.md
index 61049e3050..08372906aa 100644
--- a/README.md
+++ b/README.md
@@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-bigtable'
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-bigtable:2.49.0'
+implementation 'com.google.cloud:google-cloud-bigtable:2.50.0'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.49.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.50.0"
 ```
 
 ## Authentication
@@ -543,7 +543,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
 [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-bigtable/java11.html
 [stability-image]: https://img.shields.io/badge/stability-stable-green
 [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-bigtable.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.49.0
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.50.0
 [authentication]: https://github.com/googleapis/google-cloud-java#authentication
 [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
 [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/google-cloud-bigtable-bom/pom.xml b/google-cloud-bigtable-bom/pom.xml
index c1834f4c26..9cbdf0626d 100644
--- a/google-cloud-bigtable-bom/pom.xml
+++ b/google-cloud-bigtable-bom/pom.xml
@@ -3,12 +3,12 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-bom</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
     <packaging>pom</packaging>
     <parent>
         <groupId>com.google.cloud</groupId>
         <artifactId>sdk-platform-java-config</artifactId>
-        <version>3.40.0</version>
+        <version>3.41.0</version>
         <relativePath/>
     </parent>
 
@@ -63,37 +63,37 @@
         <dependency>
           <groupId>com.google.cloud</groupId>
           <artifactId>google-cloud-bigtable</artifactId>
-          <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+          <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         </dependency>
         <dependency>
           <groupId>com.google.cloud</groupId>
           <artifactId>google-cloud-bigtable-emulator</artifactId>
-          <version>0.186.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
+          <version>0.187.0</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
         </dependency>
         <dependency>
           <groupId>com.google.cloud</groupId>
           <artifactId>google-cloud-bigtable-emulator-core</artifactId>
-          <version>0.186.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
+          <version>0.187.0</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
         </dependency>
         <dependency>
           <groupId>com.google.api.grpc</groupId>
           <artifactId>grpc-google-cloud-bigtable-admin-v2</artifactId>
-          <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-bigtable-admin-v2:current} -->
+          <version>2.50.0</version><!-- {x-version-update:grpc-google-cloud-bigtable-admin-v2:current} -->
         </dependency>
         <dependency>
           <groupId>com.google.api.grpc</groupId>
           <artifactId>grpc-google-cloud-bigtable-v2</artifactId>
-          <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-bigtable-v2:current} -->
+          <version>2.50.0</version><!-- {x-version-update:grpc-google-cloud-bigtable-v2:current} -->
         </dependency>
         <dependency>
           <groupId>com.google.api.grpc</groupId>
           <artifactId>proto-google-cloud-bigtable-admin-v2</artifactId>
-          <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-bigtable-admin-v2:current} -->
+          <version>2.50.0</version><!-- {x-version-update:proto-google-cloud-bigtable-admin-v2:current} -->
         </dependency>
         <dependency>
           <groupId>com.google.api.grpc</groupId>
           <artifactId>proto-google-cloud-bigtable-v2</artifactId>
-          <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-bigtable-v2:current} -->
+          <version>2.50.0</version><!-- {x-version-update:proto-google-cloud-bigtable-v2:current} -->
         </dependency>
       </dependencies>
     </dependencyManagement>
diff --git a/google-cloud-bigtable-deps-bom/pom.xml b/google-cloud-bigtable-deps-bom/pom.xml
index f703a985ad..360bcebd51 100644
--- a/google-cloud-bigtable-deps-bom/pom.xml
+++ b/google-cloud-bigtable-deps-bom/pom.xml
@@ -7,13 +7,13 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>sdk-platform-java-config</artifactId>
-    <version>3.40.0</version>
+    <version>3.41.0</version>
     <relativePath/>
   </parent>
 
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-  <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+  <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
 
   <packaging>pom</packaging>
   <description>
@@ -67,7 +67,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>gapic-libraries-bom</artifactId>
-        <version>1.48.0</version>
+        <version>1.49.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/google-cloud-bigtable-emulator-core/pom.xml b/google-cloud-bigtable-emulator-core/pom.xml
index da7d0f2c3c..d8208a28c9 100644
--- a/google-cloud-bigtable-emulator-core/pom.xml
+++ b/google-cloud-bigtable-emulator-core/pom.xml
@@ -7,11 +7,11 @@
   <parent>
     <artifactId>google-cloud-bigtable-parent</artifactId>
     <groupId>com.google.cloud</groupId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
 
   <artifactId>google-cloud-bigtable-emulator-core</artifactId>
-  <version>0.186.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
+  <version>0.187.0</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
 
   <description>
     A Java wrapper for the Cloud Bigtable emulator.
diff --git a/google-cloud-bigtable-emulator/pom.xml b/google-cloud-bigtable-emulator/pom.xml
index 641d5cb0a7..55a61f951c 100644
--- a/google-cloud-bigtable-emulator/pom.xml
+++ b/google-cloud-bigtable-emulator/pom.xml
@@ -5,7 +5,7 @@
   <modelVersion>4.0.0</modelVersion>
 
   <artifactId>google-cloud-bigtable-emulator</artifactId>
-  <version>0.186.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
+  <version>0.187.0</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
   <name>Google Cloud Java - Bigtable Emulator</name>
   <url>https://github.com/googleapis/java-bigtable</url>
   <description>
@@ -14,7 +14,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-parent</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
   <scm>
     <connection>scm:git:git@github.com:googleapis/java-bigtable.git</connection>
@@ -81,14 +81,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -99,7 +99,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-bigtable-emulator-core</artifactId>
-      <version>0.186.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
+      <version>0.187.0</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
     </dependency>
 
     <dependency>
diff --git a/google-cloud-bigtable/clirr-ignored-differences.xml b/google-cloud-bigtable/clirr-ignored-differences.xml
index a3dc564c44..4052e1b4a7 100644
--- a/google-cloud-bigtable/clirr-ignored-differences.xml
+++ b/google-cloud-bigtable/clirr-ignored-differences.xml
@@ -275,4 +275,37 @@
         <className>com/google/cloud/bigtable/data/v2/stub/metrics/DefaultMetricsProvider</className>
         <method>*</method>
     </difference>
+    <difference>
+        <!-- InternalApi was updated -->
+        <differenceType>7006</differenceType>
+        <className>com/google/cloud/bigtable/data/v2/internal/*</className>
+        <method>*getTimestamp(*)</method>
+        <to>java.time.Instant</to>
+    </difference>
+    <difference>
+        <!-- BetaApi was updated -->
+        <differenceType>7006</differenceType>
+        <className>com/google/cloud/bigtable/data/v2/models/sql/StructReader</className>
+        <method>*getTimestamp(*)</method>
+        <to>java.time.Instant</to>
+    </difference>
+    <difference>
+        <!-- BetaApi was updated -->
+        <differenceType>7005</differenceType>
+        <className>com/google/cloud/bigtable/data/v2/models/sql/Statement$Builder</className>
+        <method>*setTimestampParam(java.lang.String, org.threeten.bp.Instant)</method>
+        <to>*setTimestampParam(java.lang.String, java.time.Instant)</to>
+    </difference>
+    <difference>
+        <!-- ChangeStream api is internal, only used by apache/beam-->
+        <differenceType>7013</differenceType>
+        <className>com/google/cloud/bigtable/data/v2/models/ChangeStreamMutation</className>
+        <method>*get*Time()</method>
+    </difference>
+    <difference>
+        <!-- Heartbeat is part of the ChangeStream api, which is internal and only used by apache/beam-->
+        <differenceType>7013</differenceType>
+        <className>com/google/cloud/bigtable/data/v2/models/Heartbeat</className>
+        <method>*getEstimatedLowWatermarkTime()</method>
+    </difference>
 </differences>
diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml
index 02dc0bb912..28ba7bf052 100644
--- a/google-cloud-bigtable/pom.xml
+++ b/google-cloud-bigtable/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-bigtable</artifactId>
-  <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+  <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Bigtable</name>
   <url>https://github.com/googleapis/java-bigtable</url>
@@ -12,11 +12,11 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-parent</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
   <properties>
     <!-- The version that will be embedded in the published jar via maven-resources-plugin -->
-    <java-bigtable.version>2.49.1-SNAPSHOT</java-bigtable.version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <java-bigtable.version>2.50.0</java-bigtable.version><!-- {x-version-update:google-cloud-bigtable:current} -->
 
     <site.installationModule>google-cloud-bigtable</site.installationModule>
 
@@ -52,14 +52,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java
index e76e67c842..d839ac63fa 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java
@@ -20,6 +20,6 @@
 @InternalApi("For internal use only")
 public final class Version {
   // {x-version-update-start:google-cloud-bigtable:current}
-  public static String VERSION = "2.49.1-SNAPSHOT";
+  public static String VERSION = "2.50.0";
   // {x-version-update-end}
 }
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AppProfile.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AppProfile.java
index bd7a534640..2507ef4dd3 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AppProfile.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AppProfile.java
@@ -18,7 +18,6 @@
 import com.google.api.core.InternalApi;
 import com.google.bigtable.admin.v2.AppProfile.DataBoostIsolationReadOnly;
 import com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny;
-import com.google.bigtable.admin.v2.AppProfile.Priority;
 import com.google.bigtable.admin.v2.AppProfile.StandardIsolation;
 import com.google.bigtable.admin.v2.AppProfileName;
 import com.google.common.base.Objects;
@@ -69,6 +68,10 @@ private AppProfile(@Nonnull com.google.bigtable.admin.v2.AppProfile proto) {
   @SuppressWarnings("WeakerAccess")
   public RoutingPolicy getPolicy() {
     if (proto.hasMultiClusterRoutingUseAny()) {
+      if (proto.getMultiClusterRoutingUseAny().hasRowAffinity()) {
+        return MultiClusterRoutingPolicy.withRowAffinity(
+            ImmutableSet.copyOf(proto.getMultiClusterRoutingUseAny().getClusterIdsList()));
+      }
       return MultiClusterRoutingPolicy.of(
           ImmutableSet.copyOf(proto.getMultiClusterRoutingUseAny().getClusterIdsList()));
     } else if (proto.hasSingleClusterRouting()) {
@@ -267,6 +270,34 @@ public static MultiClusterRoutingPolicy of(Set<String> clusterIds) {
           MultiClusterRoutingUseAny.newBuilder().addAllClusterIds(clusterIds).build());
     }
 
+    /** Creates a new instance of {@link MultiClusterRoutingPolicy}. */
+    public static MultiClusterRoutingPolicy withRowAffinity() {
+      return new MultiClusterRoutingPolicy(
+          MultiClusterRoutingUseAny.newBuilder()
+              .setRowAffinity(MultiClusterRoutingUseAny.RowAffinity.getDefaultInstance())
+              .build());
+    }
+
+    /**
+     * Creates a new instance of {@link MultiClusterRoutingPolicy} with row affinity enabled and
+     * specified cluster ids to route to.
+     */
+    public static MultiClusterRoutingPolicy withRowAffinity(String... clusterIds) {
+      return withRowAffinity(ImmutableSet.copyOf(clusterIds));
+    }
+
+    /**
+     * Creates a new instance of {@link MultiClusterRoutingPolicy} with specified cluster ids to
+     * route to.
+     */
+    public static MultiClusterRoutingPolicy withRowAffinity(Set<String> clusterIds) {
+      return new MultiClusterRoutingPolicy(
+          MultiClusterRoutingUseAny.newBuilder()
+              .addAllClusterIds(clusterIds)
+              .setRowAffinity(MultiClusterRoutingUseAny.RowAffinity.getDefaultInstance())
+              .build());
+    }
+
     /*
      * Returns the set of clusters to route to. The order is ignored; clusters will be
      * tried in order of distance. If empty, all clusters are eligible.
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Type.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Type.java
index df5c6dcd95..cd6a0a5407 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Type.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/common/Type.java
@@ -23,8 +23,8 @@
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.List;
-import org.threeten.bp.Instant;
 
 /**
  * Shared type implementations. Right now this is only used by SqlType but this will become a shared
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java
index 25ff2ff30d..ad1e70ba6a 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java
@@ -39,7 +39,6 @@
 import java.util.logging.Logger;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import org.threeten.bp.Duration;
 
 /**
  * Settings class to configure an instance of {@link BigtableDataClient}.
@@ -134,9 +133,11 @@ public static Builder newBuilderForEmulator(String hostname, int port) {
                 .setMaxInboundMessageSize(256 * 1024 * 1024)
                 .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1))
                 .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
-                .setKeepAliveTime(Duration.ofSeconds(61)) // sends ping in this interval
-                .setKeepAliveTimeout(
-                    Duration.ofSeconds(10)) // wait this long before considering the connection dead
+                .setKeepAliveTimeDuration(
+                    java.time.Duration.ofSeconds(61)) // sends ping in this interval
+                .setKeepAliveTimeoutDuration(
+                    java.time.Duration.ofSeconds(
+                        10)) // wait this long before considering the connection dead
                 .build());
 
     LOGGER.info("Connecting to the Bigtable emulator at " + hostname + ":" + port);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReader.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReader.java
index dff4d4e0b2..2a74fccd22 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReader.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReader.java
@@ -25,12 +25,12 @@
 import com.google.common.base.Preconditions;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.threeten.bp.Instant;
 
 @InternalApi
 public abstract class AbstractProtoStructReader implements StructReader {
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImpl.java
index 1d2bd37f2d..53044c3b37 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImpl.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImpl.java
@@ -28,10 +28,10 @@
 import com.google.cloud.bigtable.data.v2.stub.sql.SqlServerStream;
 import com.google.common.base.Preconditions;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import org.threeten.bp.Instant;
 
 /**
  * The primary implementation of a ResultSet.
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutation.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutation.java
index 23fb47bd82..838a7ec62f 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutation.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutation.java
@@ -15,7 +15,10 @@
  */
 package com.google.cloud.bigtable.data.v2.models;
 
+import static com.google.api.gax.util.TimeConversionUtils.toThreetenInstant;
+
 import com.google.api.core.InternalApi;
+import com.google.api.core.ObsoleteApi;
 import com.google.auto.value.AutoValue;
 import com.google.cloud.bigtable.data.v2.models.Range.TimestampRange;
 import com.google.cloud.bigtable.data.v2.stub.changestream.ChangeStreamRecordMerger;
@@ -23,7 +26,6 @@
 import com.google.protobuf.ByteString;
 import java.io.Serializable;
 import javax.annotation.Nonnull;
-import org.threeten.bp.Instant;
 
 /**
  * A ChangeStreamMutation represents a list of mods(represented by List<{@link Entry}>) targeted at
@@ -73,13 +75,13 @@ public enum MutationType {
   static Builder createUserMutation(
       @Nonnull ByteString rowKey,
       @Nonnull String sourceClusterId,
-      Instant commitTimestamp,
+      java.time.Instant commitTimestamp,
       int tieBreaker) {
     return builder()
         .setRowKey(rowKey)
         .setType(MutationType.USER)
         .setSourceClusterId(sourceClusterId)
-        .setCommitTimestamp(commitTimestamp)
+        .setCommitTime(commitTimestamp)
         .setTieBreaker(tieBreaker);
   }
 
@@ -89,12 +91,12 @@ static Builder createUserMutation(
    * mutation.
    */
   static Builder createGcMutation(
-      @Nonnull ByteString rowKey, Instant commitTimestamp, int tieBreaker) {
+      @Nonnull ByteString rowKey, java.time.Instant commitTimestamp, int tieBreaker) {
     return builder()
         .setRowKey(rowKey)
         .setType(MutationType.GARBAGE_COLLECTION)
         .setSourceClusterId("")
-        .setCommitTimestamp(commitTimestamp)
+        .setCommitTime(commitTimestamp)
         .setTieBreaker(tieBreaker);
   }
 
@@ -110,8 +112,14 @@ static Builder createGcMutation(
   @Nonnull
   public abstract String getSourceClusterId();
 
+  /** This method is obsolete. Use {@link #getCommitTime()} instead. */
+  @ObsoleteApi("Use getCommitTime() instead")
+  public org.threeten.bp.Instant getCommitTimestamp() {
+    return toThreetenInstant(getCommitTime());
+  }
+
   /** Get the commit timestamp of the current mutation. */
-  public abstract Instant getCommitTimestamp();
+  public abstract java.time.Instant getCommitTime();
 
   /**
    * Get the tie breaker of the current mutation. This is used to resolve conflicts when multiple
@@ -123,8 +131,14 @@ static Builder createGcMutation(
   @Nonnull
   public abstract String getToken();
 
+  /** This method is obsolete. Use {@link #getEstimatedLowWatermarkTime()} instead. */
+  @ObsoleteApi("Use getEstimatedLowWatermarkTime() instead")
+  public org.threeten.bp.Instant getEstimatedLowWatermark() {
+    return toThreetenInstant(getEstimatedLowWatermarkTime());
+  }
+
   /** Get the low watermark of the current mutation. */
-  public abstract Instant getEstimatedLowWatermark();
+  public abstract java.time.Instant getEstimatedLowWatermarkTime();
 
   /** Get the list of mods of the current mutation. */
   @Nonnull
@@ -145,7 +159,7 @@ abstract static class Builder {
 
     abstract Builder setSourceClusterId(@Nonnull String sourceClusterId);
 
-    abstract Builder setCommitTimestamp(Instant commitTimestamp);
+    abstract Builder setCommitTime(java.time.Instant commitTimestamp);
 
     abstract Builder setTieBreaker(int tieBreaker);
 
@@ -153,7 +167,7 @@ abstract static class Builder {
 
     abstract Builder setToken(@Nonnull String token);
 
-    abstract Builder setEstimatedLowWatermark(Instant estimatedLowWatermark);
+    abstract Builder setEstimatedLowWatermarkTime(java.time.Instant estimatedLowWatermark);
 
     Builder setCell(
         @Nonnull String familyName,
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordAdapter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordAdapter.java
index 40a71b1d3c..9b892b14ea 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordAdapter.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordAdapter.java
@@ -19,8 +19,8 @@
 import com.google.bigtable.v2.ReadChangeStreamResponse;
 import com.google.cloud.bigtable.data.v2.models.Range.TimestampRange;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import javax.annotation.Nonnull;
-import org.threeten.bp.Instant;
 
 /**
  * An extension point that allows end users to plug in a custom implementation of logical change
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapter.java
index d40ad7621c..54bf05cd7d 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapter.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapter.java
@@ -20,9 +20,9 @@
 import com.google.cloud.bigtable.data.v2.models.Range.TimestampRange;
 import com.google.common.base.Preconditions;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import org.threeten.bp.Instant;
 
 /**
  * Default implementation of a {@link ChangeStreamRecordAdapter} that uses {@link
@@ -112,8 +112,7 @@ public void startUserMutation(
 
     /** {@inheritDoc} */
     @Override
-    public void startGcMutation(
-        @Nonnull ByteString rowKey, Instant commitTimestamp, int tieBreaker) {
+    public void startGcMutation(ByteString rowKey, Instant commitTimestamp, int tieBreaker) {
       this.changeStreamMutationBuilder =
           ChangeStreamMutation.createGcMutation(rowKey, commitTimestamp, tieBreaker);
     }
@@ -176,9 +175,9 @@ public void finishCell() {
     /** {@inheritDoc} */
     @Override
     public ChangeStreamRecord finishChangeStreamMutation(
-        @Nonnull String token, Instant estimatedLowWatermark) {
+        String token, Instant estimatedLowWatermark) {
       this.changeStreamMutationBuilder.setToken(token);
-      this.changeStreamMutationBuilder.setEstimatedLowWatermark(estimatedLowWatermark);
+      this.changeStreamMutationBuilder.setEstimatedLowWatermarkTime(estimatedLowWatermark);
       return this.changeStreamMutationBuilder.build();
     }
 
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Heartbeat.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Heartbeat.java
index 8e3d865790..ae5507ae75 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Heartbeat.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Heartbeat.java
@@ -15,12 +15,14 @@
  */
 package com.google.cloud.bigtable.data.v2.models;
 
+import static com.google.api.gax.util.TimeConversionUtils.toThreetenInstant;
+
 import com.google.api.core.InternalApi;
+import com.google.api.core.ObsoleteApi;
 import com.google.auto.value.AutoValue;
 import com.google.bigtable.v2.ReadChangeStreamResponse;
 import java.io.Serializable;
 import javax.annotation.Nonnull;
-import org.threeten.bp.Instant;
 
 /** A simple wrapper for {@link ReadChangeStreamResponse.Heartbeat}. */
 @InternalApi("Intended for use by the BigtableIO in apache/beam only.")
@@ -29,7 +31,8 @@ public abstract class Heartbeat implements ChangeStreamRecord, Serializable {
   private static final long serialVersionUID = 7316215828353608504L;
 
   private static Heartbeat create(
-      ChangeStreamContinuationToken changeStreamContinuationToken, Instant estimatedLowWatermark) {
+      ChangeStreamContinuationToken changeStreamContinuationToken,
+      java.time.Instant estimatedLowWatermark) {
     return new AutoValue_Heartbeat(changeStreamContinuationToken, estimatedLowWatermark);
   }
 
@@ -37,7 +40,7 @@ private static Heartbeat create(
   static Heartbeat fromProto(@Nonnull ReadChangeStreamResponse.Heartbeat heartbeat) {
     return create(
         ChangeStreamContinuationToken.fromProto(heartbeat.getContinuationToken()),
-        Instant.ofEpochSecond(
+        java.time.Instant.ofEpochSecond(
             heartbeat.getEstimatedLowWatermark().getSeconds(),
             heartbeat.getEstimatedLowWatermark().getNanos()));
   }
@@ -45,6 +48,12 @@ static Heartbeat fromProto(@Nonnull ReadChangeStreamResponse.Heartbeat heartbeat
   @InternalApi("Intended for use by the BigtableIO in apache/beam only.")
   public abstract ChangeStreamContinuationToken getChangeStreamContinuationToken();
 
+  /** This method is obsolete. Use {@link #getEstimatedLowWatermarkTime()} instead. */
+  @ObsoleteApi("Use getEstimatedLowWatermarkTime() instead")
+  public org.threeten.bp.Instant getEstimatedLowWatermark() {
+    return toThreetenInstant(getEstimatedLowWatermarkTime());
+  }
+
   @InternalApi("Intended for use by the BigtableIO in apache/beam only.")
-  public abstract Instant getEstimatedLowWatermark();
+  public abstract java.time.Instant getEstimatedLowWatermarkTime();
 }
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQuery.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQuery.java
index a6dfb7666d..2c9cf54354 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQuery.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQuery.java
@@ -15,7 +15,11 @@
  */
 package com.google.cloud.bigtable.data.v2.models;
 
+import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration;
+import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeInstant;
+
 import com.google.api.core.InternalApi;
+import com.google.api.core.ObsoleteApi;
 import com.google.bigtable.v2.ReadChangeStreamRequest;
 import com.google.bigtable.v2.RowRange;
 import com.google.bigtable.v2.StreamContinuationTokens;
@@ -36,7 +40,6 @@
 import java.util.List;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import org.threeten.bp.Instant;
 
 /** A simple wrapper to construct a query for the ReadChangeStream RPC. */
 @InternalApi("Intended for use by the BigtableIO in apache/beam only.")
@@ -143,8 +146,14 @@ public ReadChangeStreamQuery streamPartition(ByteStringRange range) {
     return streamPartition(rangeBuilder.build());
   }
 
+  /** This method is obsolete. Use {@link #startTime(java.time.Instant)} instead. */
+  @ObsoleteApi("Use startTime(java.time.Instant) instead")
+  public ReadChangeStreamQuery startTime(org.threeten.bp.Instant value) {
+    return startTime(toJavaTimeInstant(value));
+  }
+
   /** Sets the startTime to read the change stream. */
-  public ReadChangeStreamQuery startTime(Instant value) {
+  public ReadChangeStreamQuery startTime(java.time.Instant value) {
     Preconditions.checkState(
         !builder.hasContinuationTokens(),
         "startTime and continuationTokens can't be specified together");
@@ -156,8 +165,14 @@ public ReadChangeStreamQuery startTime(Instant value) {
     return this;
   }
 
+  /** This method is obsolete. Use {@link #endTime(java.time.Instant)} instead. */
+  @ObsoleteApi("Use endTime(java.time.Instant) instead")
+  public ReadChangeStreamQuery endTime(org.threeten.bp.Instant value) {
+    return endTime(toJavaTimeInstant(value));
+  }
+
   /** Sets the endTime to read the change stream. */
-  public ReadChangeStreamQuery endTime(Instant value) {
+  public ReadChangeStreamQuery endTime(java.time.Instant value) {
     builder.setEndTime(
         Timestamp.newBuilder()
             .setSeconds(value.getEpochSecond())
@@ -181,8 +196,14 @@ public ReadChangeStreamQuery continuationTokens(
     return this;
   }
 
-  /** Sets the heartbeat duration for the change stream. */
+  /** This method is obsolete. Use {@link #heartbeatDuration(java.time.Duration)} instead. */
+  @ObsoleteApi("Use heartbeatDuration(java.time.Duration) instead")
   public ReadChangeStreamQuery heartbeatDuration(org.threeten.bp.Duration duration) {
+    return heartbeatDuration(toJavaTimeDuration(duration));
+  }
+
+  /** Sets the heartbeat duration for the change stream. */
+  public ReadChangeStreamQuery heartbeatDuration(java.time.Duration duration) {
     builder.setHeartbeatDuration(
         Duration.newBuilder()
             .setSeconds(duration.getSeconds())
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/SqlType.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/SqlType.java
index 50146f292a..d4d3261dcf 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/SqlType.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/SqlType.java
@@ -23,8 +23,8 @@
 import com.google.cloud.bigtable.common.Type.StructWithSchema;
 import com.google.protobuf.ByteString;
 import java.io.Serializable;
+import java.time.Instant;
 import java.util.List;
-import org.threeten.bp.Instant;
 
 /**
  * Represents a data type in a SQL query.
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/Statement.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/Statement.java
index 58c16e2c5c..c1831219a6 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/Statement.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/Statement.java
@@ -27,11 +27,11 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Timestamp;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
-import org.threeten.bp.Instant;
 
 /**
  * A SQL statement that can be executed by calling {@link
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/StructReader.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/StructReader.java
index 8f450bbd92..f127b6b54c 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/StructReader.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/sql/StructReader.java
@@ -18,9 +18,9 @@
 import com.google.api.core.BetaApi;
 import com.google.cloud.Date;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
-import org.threeten.bp.Instant;
 
 /**
  * An interface for reading the columns of a {@code Struct} or {@code
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingServerStreamingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingServerStreamingCallable.java
index 62f8b5abf6..c3b0f94ec7 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingServerStreamingCallable.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingServerStreamingCallable.java
@@ -30,13 +30,13 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Stopwatch;
 import com.google.common.util.concurrent.RateLimiter;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.logging.Logger;
 import javax.annotation.Nonnull;
-import org.threeten.bp.Duration;
-import org.threeten.bp.Instant;
 
 class RateLimitingServerStreamingCallable
     extends ServerStreamingCallable<MutateRowsRequest, MutateRowsResponse> {
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamStateMachine.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamStateMachine.java
index b41acc4ac3..27cb6f1478 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamStateMachine.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamStateMachine.java
@@ -22,7 +22,6 @@
 import com.google.cloud.bigtable.data.v2.models.Range.TimestampRange;
 import com.google.cloud.bigtable.data.v2.models.Value;
 import com.google.common.base.Preconditions;
-import org.threeten.bp.Instant;
 
 /**
  * A state machine to produce change stream records from a stream of {@link
@@ -334,7 +333,7 @@ State handleDataChange(ReadChangeStreamResponse.DataChange dataChange) {
                 "AWAITING_NEW_STREAM_RECORD: GC mutation shouldn't have source cluster id.");
             builder.startGcMutation(
                 dataChange.getRowKey(),
-                Instant.ofEpochSecond(
+                java.time.Instant.ofEpochSecond(
                     dataChange.getCommitTimestamp().getSeconds(),
                     dataChange.getCommitTimestamp().getNanos()),
                 dataChange.getTiebreaker());
@@ -345,7 +344,7 @@ State handleDataChange(ReadChangeStreamResponse.DataChange dataChange) {
             builder.startUserMutation(
                 dataChange.getRowKey(),
                 dataChange.getSourceClusterId(),
-                Instant.ofEpochSecond(
+                java.time.Instant.ofEpochSecond(
                     dataChange.getCommitTimestamp().getSeconds(),
                     dataChange.getCommitTimestamp().getNanos()),
                 dataChange.getTiebreaker());
@@ -578,7 +577,7 @@ private State checkAndFinishMutationIfNeeded(ReadChangeStreamResponse.DataChange
       completeChangeStreamRecord =
           builder.finishChangeStreamMutation(
               dataChange.getToken(),
-              Instant.ofEpochSecond(
+              java.time.Instant.ofEpochSecond(
                   dataChange.getEstimatedLowWatermark().getSeconds(),
                   dataChange.getEstimatedLowWatermark().getNanos()));
       return AWAITING_STREAM_RECORD_CONSUME;
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
index ff5bcd81c1..a829c3f719 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
@@ -40,6 +40,8 @@
 import com.google.cloud.monitoring.v3.MetricServiceClient;
 import com.google.cloud.monitoring.v3.MetricServiceSettings;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -65,7 +67,6 @@
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
-import org.threeten.bp.Duration;
 
 /**
  * Bigtable Cloud Monitoring OpenTelemetry Exporter.
@@ -97,8 +98,9 @@ public final class BigtableCloudMonitoringExporter implements MetricExporter {
 
   private final String taskId;
 
-  // The resource the client application is running on
-  private final MonitoredResource applicationResource;
+  // Application resource is initialized on the first export, which runs on a background thread
+  // to avoid slowness when starting the client.
+  private final Supplier<MonitoredResource> applicationResource;
 
   private final AtomicBoolean isShutdown = new AtomicBoolean(false);
 
@@ -144,33 +146,20 @@ public static BigtableCloudMonitoringExporter create(
       settingsBuilder.setEndpoint(endpoint);
     }
 
-    org.threeten.bp.Duration timeout = Duration.ofMinutes(1);
+    java.time.Duration timeout = java.time.Duration.ofMinutes(1);
     // TODO: createServiceTimeSeries needs special handling if the request failed. Leaving
     // it as not retried for now.
-    settingsBuilder.createServiceTimeSeriesSettings().setSimpleTimeoutNoRetries(timeout);
-
-    // Detect the resource that the client application is running on. For example,
-    // this could be a GCE instance or a GKE pod. Currently, we only support GCE instance and
-    // GKE pod. This method will return null for everything else.
-    MonitoredResource applicationResource = null;
-    try {
-      applicationResource = BigtableExporterUtils.detectResource();
-    } catch (Exception e) {
-      logger.log(
-          Level.WARNING,
-          "Failed to detect resource, will skip exporting application level metrics ",
-          e);
-    }
+    settingsBuilder.createServiceTimeSeriesSettings().setSimpleTimeoutNoRetriesDuration(timeout);
 
     return new BigtableCloudMonitoringExporter(
         MetricServiceClient.create(settingsBuilder.build()),
-        applicationResource,
+        Suppliers.memoize(BigtableExporterUtils::detectResourceSafe),
         BigtableExporterUtils.getDefaultTaskValue());
   }
 
   @VisibleForTesting
   BigtableCloudMonitoringExporter(
-      MetricServiceClient client, @Nullable MonitoredResource applicationResource, String taskId) {
+      MetricServiceClient client, Supplier<MonitoredResource> applicationResource, String taskId) {
     this.client = client;
     this.taskId = taskId;
     this.applicationResource = applicationResource;
@@ -258,7 +247,7 @@ public void onSuccess(List<Empty> emptyList) {
   /** Export metrics associated with the resource the Application is running on. */
   private CompletableResultCode exportApplicationResourceMetrics(
       Collection<MetricData> collection) {
-    if (applicationResource == null) {
+    if (applicationResource.get() == null) {
       return CompletableResultCode.ofSuccess();
     }
 
@@ -277,7 +266,7 @@ private CompletableResultCode exportApplicationResourceMetrics(
     try {
       timeSeries =
           BigtableExporterUtils.convertToApplicationResourceTimeSeries(
-              metricData, taskId, applicationResource);
+              metricData, taskId, applicationResource.get());
     } catch (Throwable e) {
       logger.log(
           Level.WARNING,
@@ -292,7 +281,8 @@ private CompletableResultCode exportApplicationResourceMetrics(
     CompletableResultCode exportCode = new CompletableResultCode();
     try {
       ProjectName projectName =
-          ProjectName.of(applicationResource.getLabelsOrThrow(APPLICATION_RESOURCE_PROJECT_ID));
+          ProjectName.of(
+              applicationResource.get().getLabelsOrThrow(APPLICATION_RESOURCE_PROJECT_ID));
 
       gceOrGkeFuture = exportTimeSeries(projectName, timeSeries);
 
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java
index 821c2295e0..95df887f0d 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java
@@ -156,7 +156,20 @@ static List<TimeSeries> convertToApplicationResourceTimeSeries(
   }
 
   @Nullable
-  static MonitoredResource detectResource() {
+  static MonitoredResource detectResourceSafe() {
+    try {
+      return detectResource();
+    } catch (Exception e) {
+      logger.log(
+          Level.WARNING,
+          "Failed to detect resource, will skip exporting application level metrics ",
+          e);
+      return null;
+    }
+  }
+
+  @Nullable
+  private static MonitoredResource detectResource() {
     GCPPlatformDetector detector = GCPPlatformDetector.DEFAULT_INSTANCE;
     DetectedPlatform detectedPlatform = detector.detectPlatform();
     MonitoredResource monitoredResource = null;
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
index 4683ff9c8e..92aa26c50c 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
@@ -16,6 +16,7 @@
 package com.google.cloud.bigtable.data.v2.stub.metrics;
 
 import static com.google.api.gax.tracing.ApiTracerFactory.OperationType;
+import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration;
 import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_NAME_KEY;
 import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLUSTER_ID_KEY;
 import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METHOD_KEY;
@@ -24,6 +25,7 @@
 import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.TABLE_ID_KEY;
 import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ZONE_ID_KEY;
 
+import com.google.api.core.ObsoleteApi;
 import com.google.api.gax.retrying.ServerStreamingAttemptException;
 import com.google.api.gax.tracing.SpanName;
 import com.google.cloud.bigtable.Version;
@@ -33,6 +35,7 @@
 import io.opentelemetry.api.common.Attributes;
 import io.opentelemetry.api.metrics.DoubleHistogram;
 import io.opentelemetry.api.metrics.LongCounter;
+import java.time.Duration;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -40,7 +43,6 @@
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.logging.Logger;
 import javax.annotation.Nullable;
-import org.threeten.bp.Duration;
 
 /**
  * A {@link BigtableTracer} that records built-in metrics and publish under the
@@ -200,8 +202,18 @@ public void attemptCancelled() {
     recordAttemptCompletion(new CancellationException());
   }
 
+  /**
+   * This method is obsolete. Use {@link #attemptFailedDuration(Throwable, java.time.Duration)}
+   * instead.
+   */
+  @ObsoleteApi("Use attemptFailedDuration(Throwable, java.time.Duration) instead")
   @Override
-  public void attemptFailed(Throwable error, Duration delay) {
+  public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
+    attemptFailedDuration(error, toJavaTimeDuration(delay));
+  }
+
+  @Override
+  public void attemptFailedDuration(Throwable error, Duration delay) {
     recordAttemptCompletion(error);
   }
 
@@ -291,7 +303,7 @@ public void setLocations(String zone, String cluster) {
 
   @Override
   public void batchRequestThrottled(long throttledTimeNanos) {
-    totalClientBlockingTime.addAndGet(Duration.ofNanos(throttledTimeNanos).toMillis());
+    totalClientBlockingTime.addAndGet(java.time.Duration.ofNanos(throttledTimeNanos).toMillis());
   }
 
   @Override
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsView.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsView.java
index 07679af8d2..68836a7e71 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsView.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsView.java
@@ -29,8 +29,11 @@
 /**
  * A util class to register built-in metrics on a custom OpenTelemetry instance. This is for
  * advanced usage, and is only necessary when wanting to write built-in metrics to cloud monitoring
- * and custom sinks. Please refer to {@link CustomOpenTelemetryMetricsProvider} for example usage.
+ * and custom sinks.
+ *
+ * @deprecated Use methods in {@link CustomOpenTelemetryMetricsProvider} instead.
  */
+@Deprecated
 public class BuiltinMetricsView {
 
   private BuiltinMetricsView() {}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java
index 7882c82d93..cb846f19b7 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java
@@ -15,12 +15,14 @@
  */
 package com.google.cloud.bigtable.data.v2.stub.metrics;
 
+import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration;
+
+import com.google.api.core.ObsoleteApi;
 import com.google.api.gax.tracing.ApiTracer;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
 import javax.annotation.Nullable;
-import org.threeten.bp.Duration;
 
 /**
  * Combines multiple {@link ApiTracer}s and {@link BigtableTracer}s into a single {@link ApiTracer}.
@@ -124,9 +126,20 @@ public void attemptCancelled() {
     }
   }
 
-  public void attemptFailed(Throwable error, Duration delay) {
+  /**
+   * This method is obsolete. Use {@link #attemptFailedDuration(Throwable, java.time.Duration)}
+   * instead.
+   */
+  @ObsoleteApi("Use attemptFailedDuration(Throwable, java.time.Duration) instead")
+  @Override
+  public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
+    attemptFailedDuration(error, toJavaTimeDuration(delay));
+  }
+
+  @Override
+  public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
     for (ApiTracer child : children) {
-      child.attemptFailed(error, delay);
+      child.attemptFailedDuration(error, delay);
     }
   }
 
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CustomOpenTelemetryMetricsProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CustomOpenTelemetryMetricsProvider.java
index d728d657ae..efcec28ffa 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CustomOpenTelemetryMetricsProvider.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CustomOpenTelemetryMetricsProvider.java
@@ -15,8 +15,11 @@
  */
 package com.google.cloud.bigtable.data.v2.stub.metrics;
 
+import com.google.auth.Credentials;
 import com.google.common.base.MoreObjects;
 import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
+import java.io.IOException;
 
 /**
  * Set a custom OpenTelemetry instance.
@@ -26,8 +29,8 @@
  * <pre>{@code
  * SdkMeterProviderBuilder sdkMeterProvider = SdkMeterProvider.builder();
  *
- * // register Builtin metrics on your meter provider with default credentials
- * BuiltinMetricsView.registerBuiltinMetrics(sdkMeterProvider);
+ * // Set up SdkMeterProvider for client side metrics
+ * CustomOpenTelemetryMetricsProvider.setupSdkMeterProvider(sdkMeterProvider);
  *
  * // register other metrics reader and views
  * sdkMeterProvider.registerMetricReader(..);
@@ -63,6 +66,32 @@ public OpenTelemetry getOpenTelemetry() {
     return otel;
   }
 
+  /**
+   * Convenient method to set up SdkMeterProviderBuilder with the default credential and endpoint.
+   */
+  public static void setupSdkMeterProvider(SdkMeterProviderBuilder builder) throws IOException {
+    setupSdkMeterProvider(builder, null, null);
+  }
+
+  /** Convenient method to set up SdkMeterProviderBuilder with a custom credential. */
+  public static void setupSdkMeterProvider(SdkMeterProviderBuilder builder, Credentials credentials)
+      throws IOException {
+    setupSdkMeterProvider(builder, credentials, null);
+  }
+
+  /** Convenient method to set up SdkMeterProviderBuilder with a custom endpoint. */
+  public static void setupSdkMeterProvider(SdkMeterProviderBuilder builder, String endpoint)
+      throws IOException {
+    setupSdkMeterProvider(builder, null, endpoint);
+  }
+
+  /** Convenient method to set up SdkMeterProviderBuilder with a custom credentials and endpoint. */
+  public static void setupSdkMeterProvider(
+      SdkMeterProviderBuilder builder, Credentials credentials, String endpoint)
+      throws IOException {
+    BuiltinMetricsView.registerBuiltinMetrics(credentials, builder, endpoint);
+  }
+
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this).add("openTelemetry", otel).toString();
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
index a2c5bdac1f..f1c6893447 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
@@ -15,6 +15,9 @@
  */
 package com.google.cloud.bigtable.data.v2.stub.metrics;
 
+import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration;
+
+import com.google.api.core.ObsoleteApi;
 import com.google.api.gax.retrying.ServerStreamingAttemptException;
 import com.google.api.gax.tracing.ApiTracerFactory.OperationType;
 import com.google.api.gax.tracing.SpanName;
@@ -32,7 +35,6 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.Nullable;
-import org.threeten.bp.Duration;
 
 class MetricsTracer extends BigtableTracer {
 
@@ -152,8 +154,18 @@ public void attemptCancelled() {
     recordAttemptCompletion(new CancellationException());
   }
 
+  /**
+   * This method is obsolete. Use {@link #attemptFailedDuration(Throwable, java.time.Duration)}
+   * instead.
+   */
+  @ObsoleteApi("Use attemptFailedDuration(Throwable, java.time.Duration) instead")
+  @Override
+  public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
+    attemptFailedDuration(error, toJavaTimeDuration(delay));
+  }
+
   @Override
-  public void attemptFailed(Throwable throwable, Duration duration) {
+  public void attemptFailedDuration(Throwable throwable, java.time.Duration duration) {
     recordAttemptCompletion(throwable);
   }
 
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/TracedBatcherUnaryCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/TracedBatcherUnaryCallable.java
index ce73d75dc1..44ba688d55 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/TracedBatcherUnaryCallable.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/TracedBatcherUnaryCallable.java
@@ -21,7 +21,6 @@
 import com.google.api.gax.rpc.ApiCallContext;
 import com.google.api.gax.rpc.UnaryCallable;
 import com.google.api.gax.tracing.ApiTracer;
-import org.threeten.bp.Duration;
 
 /**
  * This callable will extract total throttled time from {@link ApiCallContext} and add it to {@link
@@ -44,7 +43,8 @@ public ApiFuture<ResponseT> futureCall(RequestT request, ApiCallContext context)
       if (tracer instanceof BigtableTracer) {
         ((BigtableTracer) tracer)
             .batchRequestThrottled(
-                Duration.ofMillis(context.getOption(Batcher.THROTTLED_TIME_KEY)).toNanos());
+                java.time.Duration.ofMillis(context.getOption(Batcher.THROTTLED_TIME_KEY))
+                    .toNanos());
       }
     }
     return innerCallable.futureCall(request, context);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/AttemptCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/AttemptCallable.java
index 3599e1e4df..6d5c75ea99 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/AttemptCallable.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/AttemptCallable.java
@@ -24,7 +24,6 @@
 import com.google.api.gax.rpc.UnaryCallable;
 import com.google.common.base.Preconditions;
 import java.util.concurrent.Callable;
-import org.threeten.bp.Duration;
 
 // TODO: remove this once ApiResultRetryAlgorithm is added to gax.
 /**
@@ -59,9 +58,9 @@ public ResponseT call() {
 
     try {
       // Set the RPC timeout if the caller did not provide their own.
-      Duration rpcTimeout = externalFuture.getAttemptSettings().getRpcTimeout();
+      java.time.Duration rpcTimeout = externalFuture.getAttemptSettings().getRpcTimeoutDuration();
       if (!rpcTimeout.isZero() && callContext.getTimeout() == null) {
-        callContext = callContext.withTimeout(rpcTimeout);
+        callContext = callContext.withTimeoutDuration(rpcTimeout);
       }
 
       externalFuture.setAttemptFuture(new NonCancellableFuture<ResponseT>());
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/RetryInfoRetryAlgorithm.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/RetryInfoRetryAlgorithm.java
index 085b48bbb5..98e549cee1 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/RetryInfoRetryAlgorithm.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/RetryInfoRetryAlgorithm.java
@@ -22,8 +22,7 @@
 import com.google.api.gax.rpc.ApiException;
 import com.google.protobuf.util.Durations;
 import com.google.rpc.RetryInfo;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.threeten.bp.Duration;
+import javax.annotation.Nullable;
 
 // TODO move this algorithm to gax
 /**
@@ -36,11 +35,11 @@ public class RetryInfoRetryAlgorithm<ResponseT> extends BasicResultRetryAlgorith
   @Override
   public TimedAttemptSettings createNextAttempt(
       Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings prevSettings) {
-    Duration retryDelay = extractRetryDelay(prevThrowable);
+    java.time.Duration retryDelay = extractRetryDelay(prevThrowable);
     if (retryDelay != null) {
       return prevSettings
           .toBuilder()
-          .setRandomizedRetryDelay(retryDelay)
+          .setRandomizedRetryDelayDuration(retryDelay)
           .setAttemptCount(prevSettings.getAttemptCount() + 1)
           .setOverallAttemptCount(prevSettings.getAttemptCount() + 1)
           .build();
@@ -81,8 +80,7 @@ public boolean shouldRetry(
         && ((ApiException) previousThrowable).isRetryable();
   }
 
-  @Nullable
-  static Duration extractRetryDelay(@Nullable Throwable throwable) {
+  static java.time.Duration extractRetryDelay(@Nullable Throwable throwable) {
     if (throwable == null) {
       return null;
     }
@@ -97,6 +95,6 @@ static Duration extractRetryDelay(@Nullable Throwable throwable) {
       return null;
     }
     RetryInfo retryInfo = exception.getErrorDetails().getRetryInfo();
-    return Duration.ofMillis(Durations.toMillis(retryInfo.getRetryDelay()));
+    return java.time.Duration.ofMillis(Durations.toMillis(retryInfo.getRetryDelay()));
   }
 }
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientTests.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientTests.java
index d8522db71a..388631d93a 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientTests.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientTests.java
@@ -65,7 +65,9 @@
 import com.google.protobuf.FieldMask;
 import io.grpc.Status;
 import io.grpc.Status.Code;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.Before;
@@ -1034,6 +1036,153 @@ public void testCreateAppProfileAddPriority() {
     assertThat(actualResult).isEqualTo(AppProfile.fromProto(expectedResponse));
   }
 
+  @Test
+  public void testCreateAppProfileAddRowAffinity() {
+    // Setup
+    Mockito.when(mockStub.createAppProfileCallable()).thenReturn(mockCreateAppProfileCallable);
+
+    com.google.bigtable.admin.v2.CreateAppProfileRequest expectedRequest =
+        com.google.bigtable.admin.v2.CreateAppProfileRequest.newBuilder()
+            .setParent(NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID))
+            .setAppProfileId(APP_PROFILE_ID)
+            .setAppProfile(
+                com.google.bigtable.admin.v2.AppProfile.newBuilder()
+                    .setDescription("my description")
+                    .setMultiClusterRoutingUseAny(
+                        com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                            .newBuilder()
+                            .setRowAffinity(
+                                com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                                    .RowAffinity.getDefaultInstance())))
+            .build();
+
+    com.google.bigtable.admin.v2.AppProfile expectedResponse =
+        com.google.bigtable.admin.v2.AppProfile.newBuilder()
+            .setName(APP_PROFILE_NAME)
+            .setDescription("my description")
+            .setMultiClusterRoutingUseAny(
+                com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny.newBuilder()
+                    .setRowAffinity(
+                        com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                            .RowAffinity.getDefaultInstance()))
+            .build();
+
+    Mockito.when(mockCreateAppProfileCallable.futureCall(expectedRequest))
+        .thenReturn(ApiFutures.immediateFuture(expectedResponse));
+
+    // Execute
+    AppProfile actualResult =
+        adminClient.createAppProfile(
+            CreateAppProfileRequest.of(INSTANCE_ID, APP_PROFILE_ID)
+                .setDescription("my description")
+                .setRoutingPolicy(MultiClusterRoutingPolicy.withRowAffinity()));
+
+    // Verify
+    assertThat(actualResult).isEqualTo(AppProfile.fromProto(expectedResponse));
+  }
+
+  @Test
+  public void testCreateAppProfileAddRowAffinityAddMultipleClusterIds() {
+    // Setup
+    Mockito.when(mockStub.createAppProfileCallable()).thenReturn(mockCreateAppProfileCallable);
+
+    com.google.bigtable.admin.v2.CreateAppProfileRequest expectedRequest =
+        com.google.bigtable.admin.v2.CreateAppProfileRequest.newBuilder()
+            .setParent(NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID))
+            .setAppProfileId(APP_PROFILE_ID)
+            .setAppProfile(
+                com.google.bigtable.admin.v2.AppProfile.newBuilder()
+                    .setDescription("my description")
+                    .setMultiClusterRoutingUseAny(
+                        com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                            .newBuilder()
+                            .addClusterIds("cluster-id-1")
+                            .addClusterIds("cluster-id-2")
+                            .setRowAffinity(
+                                com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                                    .RowAffinity.getDefaultInstance())))
+            .build();
+
+    com.google.bigtable.admin.v2.AppProfile expectedResponse =
+        com.google.bigtable.admin.v2.AppProfile.newBuilder()
+            .setName(APP_PROFILE_NAME)
+            .setDescription("my description")
+            .setMultiClusterRoutingUseAny(
+                com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny.newBuilder()
+                    .addClusterIds("cluster-id-1")
+                    .addClusterIds("cluster-id-2")
+                    .setRowAffinity(
+                        com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                            .RowAffinity.getDefaultInstance()))
+            .build();
+
+    Mockito.when(mockCreateAppProfileCallable.futureCall(expectedRequest))
+        .thenReturn(ApiFutures.immediateFuture(expectedResponse));
+
+    // Execute
+    AppProfile actualResult =
+        adminClient.createAppProfile(
+            CreateAppProfileRequest.of(INSTANCE_ID, APP_PROFILE_ID)
+                .setDescription("my description")
+                .setRoutingPolicy(
+                    MultiClusterRoutingPolicy.withRowAffinity("cluster-id-1", "cluster-id-2")));
+
+    // Verify
+    assertThat(actualResult).isEqualTo(AppProfile.fromProto(expectedResponse));
+  }
+
+  @Test
+  public void testCreateAppProfileAddRowAffinityAddSetOfClusterIds() {
+    // Setup
+    Mockito.when(mockStub.createAppProfileCallable()).thenReturn(mockCreateAppProfileCallable);
+
+    com.google.bigtable.admin.v2.CreateAppProfileRequest expectedRequest =
+        com.google.bigtable.admin.v2.CreateAppProfileRequest.newBuilder()
+            .setParent(NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID))
+            .setAppProfileId(APP_PROFILE_ID)
+            .setAppProfile(
+                com.google.bigtable.admin.v2.AppProfile.newBuilder()
+                    .setDescription("my description")
+                    .setMultiClusterRoutingUseAny(
+                        com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                            .newBuilder()
+                            .addClusterIds("cluster-id-1")
+                            .addClusterIds("cluster-id-2")
+                            .setRowAffinity(
+                                com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                                    .RowAffinity.getDefaultInstance())))
+            .build();
+
+    com.google.bigtable.admin.v2.AppProfile expectedResponse =
+        com.google.bigtable.admin.v2.AppProfile.newBuilder()
+            .setName(APP_PROFILE_NAME)
+            .setDescription("my description")
+            .setMultiClusterRoutingUseAny(
+                com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny.newBuilder()
+                    .addClusterIds("cluster-id-1")
+                    .addClusterIds("cluster-id-2")
+                    .setRowAffinity(
+                        com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                            .RowAffinity.getDefaultInstance()))
+            .build();
+
+    Mockito.when(mockCreateAppProfileCallable.futureCall(expectedRequest))
+        .thenReturn(ApiFutures.immediateFuture(expectedResponse));
+
+    // Execute
+    Set<String> clusterIds = new HashSet<String>();
+    clusterIds.add("cluster-id-1");
+    clusterIds.add("cluster-id-2");
+    AppProfile actualResult =
+        adminClient.createAppProfile(
+            CreateAppProfileRequest.of(INSTANCE_ID, APP_PROFILE_ID)
+                .setDescription("my description")
+                .setRoutingPolicy(MultiClusterRoutingPolicy.withRowAffinity(clusterIds)));
+
+    // Verify
+    assertThat(actualResult).isEqualTo(AppProfile.fromProto(expectedResponse));
+  }
+
   @Test
   public void testGetAppProfile() {
     // Setup
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java
index 0ba472f783..15713b17f1 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java
@@ -368,6 +368,8 @@ public void testUpdateTable() {
     assertThat(actualResult.getId()).isEqualTo(TABLE_ID);
     assertThat(actualResult.getChangeStreamRetention())
         .isEqualTo(org.threeten.bp.Duration.ofHours(24));
+    assertThat(actualResult.getChangeStreamRetention().toMillis())
+        .isEqualTo(actualResult.getChangeStreamRetention().toMillis());
   }
 
   @Test
@@ -1015,6 +1017,7 @@ public void testCopyBackup() {
     String srcTableId = "src-table";
     String srcClusterId = "src-cluster";
     String srcBackupId = "src-backup";
+
     Instant expireTime = Instant.now().plus(org.threeten.bp.Duration.ofDays(15));
     long sizeBytes = 123456789;
 
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminSettingsTest.java
index 506f73700b..735083bfee 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminSettingsTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminSettingsTest.java
@@ -31,7 +31,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
-import org.threeten.bp.Duration;
 
 @RunWith(JUnit4.class)
 public class BigtableTableAdminSettingsTest {
@@ -165,7 +164,9 @@ public void testToString() throws IOException {
     stubSettings
         .getBackupSettings()
         .setRetrySettings(
-            RetrySettings.newBuilder().setTotalTimeout(Duration.ofMinutes(812)).build());
+            RetrySettings.newBuilder()
+                .setTotalTimeout(org.threeten.bp.Duration.ofMinutes(812))
+                .build());
 
     BigtableTableAdminSettings settings = builder.build();
     checkToString(settings);
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableInstanceAdminClientIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableInstanceAdminClientIT.java
index c95afa9eef..93e8f5b790 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableInstanceAdminClientIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableInstanceAdminClientIT.java
@@ -285,6 +285,43 @@ public void appProfileTestDataBoost() {
     }
   }
 
+  @Test
+  public void appProfileTestRowAffinity() {
+    String newInstanceId = prefixGenerator.newPrefix();
+    String newClusterId = newInstanceId + "-c1";
+    String newClusterId2 = newInstanceId + "-c2";
+
+    client.createInstance(
+        CreateInstanceRequest.of(newInstanceId)
+            .addCluster(newClusterId, testEnvRule.env().getPrimaryZone(), 1, StorageType.SSD)
+            .addCluster(newClusterId2, testEnvRule.env().getSecondaryZone(), 1, StorageType.SSD)
+            .setDisplayName("Row-Affinity-Instance-Test")
+            .addLabel("state", "readytodelete")
+            .setType(Type.PRODUCTION));
+
+    try {
+      assertThat(client.exists(newInstanceId)).isTrue();
+
+      String testAppProfile = prefixGenerator.newPrefix();
+
+      CreateAppProfileRequest request =
+          CreateAppProfileRequest.of(newInstanceId, testAppProfile)
+              .setRoutingPolicy(
+                  AppProfile.MultiClusterRoutingPolicy.withRowAffinity(newClusterId, newClusterId2))
+              .setDescription("row affinity app profile");
+
+      AppProfile newlyCreateAppProfile = client.createAppProfile(request);
+      AppProfile.RoutingPolicy routingPolicy = newlyCreateAppProfile.getPolicy();
+      assertThat(routingPolicy)
+          .isEqualTo(
+              AppProfile.MultiClusterRoutingPolicy.withRowAffinity(newClusterId, newClusterId2));
+    } finally {
+      if (client.exists(newInstanceId)) {
+        client.deleteInstance(newInstanceId);
+      }
+    }
+  }
+
   @Test
   public void iamUpdateTest() {
     Policy policy = client.getIamPolicy(instanceId);
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AppProfileTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AppProfileTest.java
index 8215e5f8fc..d6e6e410e8 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AppProfileTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AppProfileTest.java
@@ -291,4 +291,54 @@ public void testFromProtoWithDataBoostIsolation() {
             AppProfile.DataBoostIsolationReadOnlyPolicy.of(
                 AppProfile.ComputeBillingOwner.UNSPECIFIED));
   }
+
+  @Test
+  public void testFromProtoWithRowAffinityNoClusterGroup() {
+    AppProfile profile =
+        AppProfile.fromProto(
+            com.google.bigtable.admin.v2.AppProfile.newBuilder()
+                .setName(AppProfileName.of("my-project", "my-instance", "my-profile").toString())
+                .setDescription("my description")
+                .setMultiClusterRoutingUseAny(
+                    com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny.newBuilder()
+                        .setRowAffinity(
+                            com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                                .RowAffinity.getDefaultInstance())
+                        .build())
+                .setEtag("my-etag")
+                .build());
+
+    assertThat(profile.getInstanceId()).isEqualTo("my-instance");
+    assertThat(profile.getId()).isEqualTo("my-profile");
+    assertThat(profile.getDescription()).isEqualTo("my description");
+    System.out.println(profile.getPolicy());
+    System.out.println(AppProfile.MultiClusterRoutingPolicy.withRowAffinity());
+    assertThat(profile.getPolicy())
+        .isEqualTo(AppProfile.MultiClusterRoutingPolicy.withRowAffinity());
+  }
+
+  @Test
+  public void testFromProtoWithRowAffinityClusterGroup() {
+    AppProfile profile =
+        AppProfile.fromProto(
+            com.google.bigtable.admin.v2.AppProfile.newBuilder()
+                .setName(AppProfileName.of("my-project", "my-instance", "my-profile").toString())
+                .setDescription("my description")
+                .setMultiClusterRoutingUseAny(
+                    com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny.newBuilder()
+                        .addAllClusterIds(ImmutableList.of("cluster-id-1", "cluster-id-2"))
+                        .setRowAffinity(
+                            com.google.bigtable.admin.v2.AppProfile.MultiClusterRoutingUseAny
+                                .RowAffinity.getDefaultInstance())
+                        .build())
+                .setEtag("my-etag")
+                .build());
+
+    assertThat(profile.getInstanceId()).isEqualTo("my-instance");
+    assertThat(profile.getId()).isEqualTo("my-profile");
+    assertThat(profile.getDescription()).isEqualTo("my description");
+    assertThat(profile.getPolicy())
+        .isEqualTo(
+            AppProfile.MultiClusterRoutingPolicy.withRowAffinity("cluster-id-1", "cluster-id-2"));
+  }
 }
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAppProfileRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAppProfileRequestTest.java
index 088dc2bcfe..7e9cc81541 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAppProfileRequestTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAppProfileRequestTest.java
@@ -101,4 +101,17 @@ public void testDataBoostIsolationReadOnly() {
                 .setComputeBillingOwner(DataBoostIsolationReadOnly.ComputeBillingOwner.HOST_PAYS)
                 .build());
   }
+
+  @Test
+  public void testRowAffinity() {
+    CreateAppProfileRequest wrapper =
+        CreateAppProfileRequest.of("my-instance", "my-profile")
+            .setRoutingPolicy(MultiClusterRoutingPolicy.withRowAffinity());
+
+    assertThat(wrapper.toProto("my-project").getAppProfile().getMultiClusterRoutingUseAny())
+        .isEqualTo(
+            MultiClusterRoutingUseAny.newBuilder()
+                .setRowAffinity(MultiClusterRoutingUseAny.RowAffinity.getDefaultInstance())
+                .build());
+  }
 }
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAppProfileRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAppProfileRequestTest.java
index 04cf3f0813..603943c533 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAppProfileRequestTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAppProfileRequestTest.java
@@ -146,4 +146,34 @@ public void testUpdateExistingDataBoostIsolationReadOnly() {
                 .setUpdateMask(FieldMask.newBuilder().addPaths("data_boost_isolation_read_only"))
                 .build());
   }
+
+  @Test
+  public void testUpdateRowAffinity() {
+    com.google.bigtable.admin.v2.AppProfile existingProto =
+        com.google.bigtable.admin.v2.AppProfile.newBuilder()
+            .setName("projects/my-project/instances/my-instance/appProfiles/my-profile")
+            .setEtag("my-etag")
+            .setDescription("description")
+            .setMultiClusterRoutingUseAny(MultiClusterRoutingUseAny.getDefaultInstance())
+            .build();
+
+    AppProfile existingWrapper = AppProfile.fromProto(existingProto);
+
+    UpdateAppProfileRequest updateWrapper =
+        UpdateAppProfileRequest.of(existingWrapper)
+            .setRoutingPolicy(AppProfile.MultiClusterRoutingPolicy.withRowAffinity());
+
+    assertThat(updateWrapper.toProto("my-project"))
+        .isEqualTo(
+            com.google.bigtable.admin.v2.UpdateAppProfileRequest.newBuilder()
+                .setAppProfile(
+                    existingProto
+                        .toBuilder()
+                        .setMultiClusterRoutingUseAny(
+                            MultiClusterRoutingUseAny.newBuilder()
+                                .setRowAffinity(
+                                    MultiClusterRoutingUseAny.RowAffinity.getDefaultInstance())))
+                .setUpdateMask(FieldMask.newBuilder().addPaths("multi_cluster_routing_use_any"))
+                .build());
+  }
 }
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReaderTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReaderTest.java
index 95955bab94..8770880983 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReaderTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/AbstractProtoStructReaderTest.java
@@ -57,6 +57,7 @@
 import com.google.cloud.bigtable.data.v2.models.sql.Struct;
 import com.google.cloud.bigtable.data.v2.stub.sql.SqlProtoFactory;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -71,7 +72,6 @@
 import org.junit.runners.JUnit4;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
-import org.threeten.bp.Instant;
 
 @RunWith(Enclosed.class)
 public class AbstractProtoStructReaderTest {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ProtoStructTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ProtoStructTest.java
index 6fa71e7fff..66808bb98a 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ProtoStructTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ProtoStructTest.java
@@ -47,13 +47,13 @@
 import com.google.cloud.Date;
 import com.google.cloud.bigtable.data.v2.models.sql.SqlType;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class ProtoStructTest {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImplTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImplTest.java
index a5b823b205..a8c5776a87 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImplTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/ResultSetImplTest.java
@@ -54,6 +54,7 @@
 import com.google.cloud.bigtable.data.v2.stub.sql.SqlServerStreamImpl;
 import com.google.cloud.bigtable.gaxx.testing.FakeStreamingApi.ServerStreamingStashCallable;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -62,7 +63,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class ResultSetImplTest {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BuiltinMetricsIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BuiltinMetricsIT.java
index d929627e12..d01ecd0575 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BuiltinMetricsIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BuiltinMetricsIT.java
@@ -33,7 +33,6 @@
 import com.google.cloud.bigtable.data.v2.models.Row;
 import com.google.cloud.bigtable.data.v2.models.RowMutation;
 import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
-import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsView;
 import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider;
 import com.google.cloud.bigtable.test_helpers.env.CloudEnv;
 import com.google.cloud.bigtable.test_helpers.env.PrefixGenerator;
@@ -60,6 +59,8 @@
 import io.opentelemetry.sdk.metrics.data.MetricData;
 import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -76,8 +77,6 @@
 import org.junit.rules.Timeout;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Duration;
-import org.threeten.bp.Instant;
 
 @Ignore("Temporarily disable flaky test")
 @RunWith(JUnit4.class)
@@ -135,7 +134,7 @@ public void setup() throws IOException {
 
     SdkMeterProviderBuilder meterProvider =
         SdkMeterProvider.builder().registerMetricReader(metricReader);
-    BuiltinMetricsView.registerBuiltinMetrics(testEnvRule.env().getProjectId(), meterProvider);
+    CustomOpenTelemetryMetricsProvider.setupSdkMeterProvider(meterProvider);
     OpenTelemetry openTelemetry =
         OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
 
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java
index 620d290338..34d0952401 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java
@@ -31,6 +31,7 @@
 import com.google.cloud.bigtable.test_helpers.env.TestEnvRule;
 import com.google.protobuf.ByteString;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -41,7 +42,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class ExecuteQueryIT {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/StreamingMetricsMetadataIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/StreamingMetricsMetadataIT.java
index 11da6a6c15..2685819f08 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/StreamingMetricsMetadataIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/StreamingMetricsMetadataIT.java
@@ -29,7 +29,6 @@
 import com.google.cloud.bigtable.data.v2.models.Query;
 import com.google.cloud.bigtable.data.v2.models.Row;
 import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
-import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsView;
 import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider;
 import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv;
 import com.google.cloud.bigtable.test_helpers.env.TestEnvRule;
@@ -72,7 +71,7 @@ public void setup() throws IOException {
 
     SdkMeterProviderBuilder meterProvider =
         SdkMeterProvider.builder().registerMetricReader(metricReader);
-    BuiltinMetricsView.registerBuiltinMetrics(testEnvRule.env().getProjectId(), meterProvider);
+    CustomOpenTelemetryMetricsProvider.setupSdkMeterProvider(meterProvider);
     OpenTelemetry openTelemetry =
         OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
 
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/UnaryMetricsMetadataIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/UnaryMetricsMetadataIT.java
index a6e4f9e88b..0196614299 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/UnaryMetricsMetadataIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/UnaryMetricsMetadataIT.java
@@ -28,7 +28,6 @@
 import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
 import com.google.cloud.bigtable.data.v2.models.RowMutation;
 import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
-import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsView;
 import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider;
 import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv;
 import com.google.cloud.bigtable.test_helpers.env.TestEnvRule;
@@ -71,7 +70,7 @@ public void setup() throws IOException {
 
     SdkMeterProviderBuilder meterProvider =
         SdkMeterProvider.builder().registerMetricReader(metricReader);
-    BuiltinMetricsView.registerBuiltinMetrics(testEnvRule.env().getProjectId(), meterProvider);
+    CustomOpenTelemetryMetricsProvider.setupSdkMeterProvider(meterProvider);
     OpenTelemetry openTelemetry =
         OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
 
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutationTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutationTest.java
index 61c028cdb6..761bec3765 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutationTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamMutationTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.cloud.bigtable.data.v2.models;
 
+import static com.google.api.gax.util.TimeConversionUtils.toThreetenInstant;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.bigtable.v2.MutateRowRequest;
@@ -29,11 +30,11 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.time.Instant;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class ChangeStreamMutationTest {
@@ -45,6 +46,10 @@ public class ChangeStreamMutationTest {
       RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID);
   private static final Instant FAKE_COMMIT_TIMESTAMP = Instant.ofEpochSecond(0, 1000L);
   private static final Instant FAKE_LOW_WATERMARK = Instant.ofEpochSecond(0, 2000L);
+  private static final org.threeten.bp.Instant FAKE_COMMIT_TIMESTAMP_THREETEN =
+      toThreetenInstant(FAKE_COMMIT_TIMESTAMP);
+  private static final org.threeten.bp.Instant FAKE_LOW_WATERMARK_THREETEN =
+      toThreetenInstant(FAKE_LOW_WATERMARK);
 
   @Test
   public void userInitiatedMutationTest() throws IOException, ClassNotFoundException {
@@ -73,17 +78,20 @@ public void userInitiatedMutationTest() throws IOException, ClassNotFoundExcepti
                 Value.rawTimestamp(1000),
                 Value.rawValue(ByteString.copyFrom(Longs.toByteArray(1234L))))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Test the getters.
     assertThat(changeStreamMutation.getRowKey()).isEqualTo(ByteString.copyFromUtf8("key"));
     assertThat(changeStreamMutation.getType()).isEqualTo(ChangeStreamMutation.MutationType.USER);
     assertThat(changeStreamMutation.getSourceClusterId()).isEqualTo("fake-source-cluster-id");
-    assertThat(changeStreamMutation.getCommitTimestamp()).isEqualTo(FAKE_COMMIT_TIMESTAMP);
+    assertThat(changeStreamMutation.getCommitTime()).isEqualTo(FAKE_COMMIT_TIMESTAMP);
+    assertThat(changeStreamMutation.getCommitTimestamp()).isEqualTo(FAKE_COMMIT_TIMESTAMP_THREETEN);
     assertThat(changeStreamMutation.getTieBreaker()).isEqualTo(0);
     assertThat(changeStreamMutation.getToken()).isEqualTo("fake-token");
-    assertThat(changeStreamMutation.getEstimatedLowWatermark()).isEqualTo(FAKE_LOW_WATERMARK);
+    assertThat(changeStreamMutation.getEstimatedLowWatermarkTime()).isEqualTo(FAKE_LOW_WATERMARK);
+    assertThat(changeStreamMutation.getEstimatedLowWatermark())
+        .isEqualTo(FAKE_LOW_WATERMARK_THREETEN);
 
     // Test serialization.
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -112,7 +120,7 @@ public void gcMutationTest() throws IOException, ClassNotFoundException {
                 ByteString.copyFromUtf8("fake-qualifier"),
                 Range.TimestampRange.create(1000L, 2000L))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Test the getters.
@@ -120,10 +128,13 @@ public void gcMutationTest() throws IOException, ClassNotFoundException {
     assertThat(changeStreamMutation.getType())
         .isEqualTo(ChangeStreamMutation.MutationType.GARBAGE_COLLECTION);
     Assert.assertTrue(changeStreamMutation.getSourceClusterId().isEmpty());
-    assertThat(changeStreamMutation.getCommitTimestamp()).isEqualTo(FAKE_COMMIT_TIMESTAMP);
+    assertThat(changeStreamMutation.getCommitTime()).isEqualTo(FAKE_COMMIT_TIMESTAMP);
+    assertThat(changeStreamMutation.getCommitTimestamp()).isEqualTo(FAKE_COMMIT_TIMESTAMP_THREETEN);
     assertThat(changeStreamMutation.getTieBreaker()).isEqualTo(0);
     assertThat(changeStreamMutation.getToken()).isEqualTo("fake-token");
-    assertThat(changeStreamMutation.getEstimatedLowWatermark()).isEqualTo(FAKE_LOW_WATERMARK);
+    assertThat(changeStreamMutation.getEstimatedLowWatermarkTime()).isEqualTo(FAKE_LOW_WATERMARK);
+    assertThat(changeStreamMutation.getEstimatedLowWatermark())
+        .isEqualTo(FAKE_LOW_WATERMARK_THREETEN);
 
     // Test serialization.
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -161,7 +172,7 @@ public void toRowMutationTest() {
                 Value.rawTimestamp(1000),
                 Value.rawValue(ByteString.copyFrom(Longs.toByteArray(1234L))))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Convert it to a rowMutation and construct a MutateRowRequest.
@@ -204,7 +215,7 @@ public void toRowMutationWithoutTokenShouldFailTest() {
         ChangeStreamMutation.createUserMutation(
                 ByteString.copyFromUtf8("key"), "fake-source-cluster-id", FAKE_COMMIT_TIMESTAMP, 0)
             .deleteFamily("fake-family")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK);
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK);
     Assert.assertThrows(IllegalStateException.class, builder::build);
   }
 
@@ -244,7 +255,7 @@ public void toRowMutationEntryTest() {
                 Value.rawTimestamp(1000),
                 Value.rawValue(ByteString.copyFrom(Longs.toByteArray(1234L))))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Convert it to a rowMutationEntry and construct a MutateRowRequest.
@@ -284,7 +295,7 @@ public void toRowMutationEntryWithoutTokenShouldFailTest() {
         ChangeStreamMutation.createUserMutation(
                 ByteString.copyFromUtf8("key"), "fake-source-cluster-id", FAKE_COMMIT_TIMESTAMP, 0)
             .deleteFamily("fake-family")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK);
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK);
     Assert.assertThrows(IllegalStateException.class, builder::build);
   }
 
@@ -309,7 +320,7 @@ public void testWithLongValue() {
                 1000L,
                 ByteString.copyFrom(Longs.toByteArray(1L)))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     RowMutation rowMutation = changeStreamMutation.toRowMutation(TABLE_ID);
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordTest.java
index 3f09d9b443..9dd66acc73 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ChangeStreamRecordTest.java
@@ -30,6 +30,7 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.time.Instant;
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
@@ -37,7 +38,6 @@
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class ChangeStreamRecordTest {
@@ -129,7 +129,7 @@ public void heartbeatTest() {
             .build();
     Heartbeat actualHeartbeat = Heartbeat.fromProto(heartbeatProto);
 
-    assertThat(actualHeartbeat.getEstimatedLowWatermark())
+    assertThat(actualHeartbeat.getEstimatedLowWatermarkTime())
         .isEqualTo(Instant.ofEpochSecond(lowWatermark.getSeconds(), lowWatermark.getNanos()));
     assertThat(actualHeartbeat.getChangeStreamContinuationToken().getPartition())
         .isEqualTo(ByteStringRange.create(rowRange.getStartKeyClosed(), rowRange.getEndKeyOpen()));
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapterTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapterTest.java
index 22270bc269..b6997ae9dd 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapterTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/DefaultChangeStreamRecordAdapterTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.cloud.bigtable.data.v2.models;
 
+import static com.google.api.gax.util.TimeConversionUtils.toThreetenInstant;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.bigtable.v2.Mutation;
@@ -25,6 +26,7 @@
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Timestamp;
 import com.google.rpc.Status;
+import java.time.Instant;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -32,7 +34,6 @@
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class DefaultChangeStreamRecordAdapterTest {
@@ -41,6 +42,8 @@ public class DefaultChangeStreamRecordAdapterTest {
   private ChangeStreamRecordBuilder<ChangeStreamRecord> changeStreamRecordBuilder;
   private static final Instant FAKE_COMMIT_TIMESTAMP = Instant.ofEpochSecond(0L, 1000L);
   private static final Instant FAKE_LOW_WATERMARK = Instant.ofEpochSecond(0L, 2000L);
+  private static final org.threeten.bp.Instant FAKE_LOW_WATERMARK_THREETEN =
+      toThreetenInstant(FAKE_LOW_WATERMARK);
 
   @Rule public ExpectedException expect = ExpectedException.none();
 
@@ -59,7 +62,7 @@ public void isHeartbeatTest() {
         ChangeStreamMutation.createGcMutation(
                 ByteString.copyFromUtf8("key"), FAKE_COMMIT_TIMESTAMP, 0)
             .setToken("token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
     Assert.assertTrue(adapter.isHeartbeat(heartbeatRecord));
     Assert.assertFalse(adapter.isHeartbeat(closeStreamRecord));
@@ -73,8 +76,8 @@ public void getTokenFromHeartbeatTest() {
             ReadChangeStreamResponse.Heartbeat.newBuilder()
                 .setEstimatedLowWatermark(
                     Timestamp.newBuilder()
-                        .setSeconds(FAKE_LOW_WATERMARK.getEpochSecond())
-                        .setNanos(FAKE_LOW_WATERMARK.getNano()))
+                        .setSeconds(FAKE_LOW_WATERMARK_THREETEN.getEpochSecond())
+                        .setNanos(FAKE_LOW_WATERMARK_THREETEN.getNano()))
                 .setContinuationToken(
                     StreamContinuationToken.newBuilder().setToken("heartbeat-token").build())
                 .build());
@@ -99,7 +102,7 @@ public void isChangeStreamMutationTest() {
         ChangeStreamMutation.createGcMutation(
                 ByteString.copyFromUtf8("key"), FAKE_COMMIT_TIMESTAMP, 0)
             .setToken("token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
     Assert.assertFalse(adapter.isChangeStreamMutation(heartbeatRecord));
     Assert.assertFalse(adapter.isChangeStreamMutation(closeStreamRecord));
@@ -112,7 +115,7 @@ public void getTokenFromChangeStreamMutationTest() {
         ChangeStreamMutation.createGcMutation(
                 ByteString.copyFromUtf8("key"), FAKE_COMMIT_TIMESTAMP, 0)
             .setToken("change-stream-mutation-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
     Assert.assertEquals(
         adapter.getTokenFromChangeStreamMutation(changeStreamMutationRecord),
@@ -133,8 +136,8 @@ public void heartbeatTest() {
         ReadChangeStreamResponse.Heartbeat.newBuilder()
             .setEstimatedLowWatermark(
                 Timestamp.newBuilder()
-                    .setSeconds(FAKE_LOW_WATERMARK.getEpochSecond())
-                    .setNanos(FAKE_LOW_WATERMARK.getNano())
+                    .setSeconds(FAKE_LOW_WATERMARK_THREETEN.getEpochSecond())
+                    .setNanos(FAKE_LOW_WATERMARK_THREETEN.getNano())
                     .build())
             .setContinuationToken(
                 StreamContinuationToken.newBuilder().setToken("random-token").build())
@@ -186,7 +189,7 @@ public void singleDeleteFamilyTest() {
                 ByteString.copyFromUtf8("key"), "fake-source-cluster-id", FAKE_COMMIT_TIMESTAMP, 0)
             .deleteFamily("fake-family")
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Create the ChangeStreamMutation through the ChangeStreamRecordBuilder.
@@ -225,7 +228,7 @@ public void singleDeleteCellTest() {
                 ByteString.copyFromUtf8("fake-qualifier"),
                 Range.TimestampRange.create(1000L, 2000L))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Create the ChangeStreamMutation through the ChangeStreamRecordBuilder.
@@ -258,7 +261,7 @@ public void singleNonChunkedCellTest() {
                 100L,
                 ByteString.copyFromUtf8("fake-value"))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Create the ChangeStreamMutation through the ChangeStreamRecordBuilder.
@@ -290,7 +293,7 @@ public void singleChunkedCellTest() {
                 100L,
                 ByteString.copyFromUtf8("fake-value1-value2"))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     // Create the ChangeStreamMutation through the ChangeStreamRecordBuilder.
@@ -327,7 +330,7 @@ public void multipleChunkedCellsTest() {
     }
     expectedChangeStreamMutationBuilder
         .setToken("fake-token")
-        .setEstimatedLowWatermark(FAKE_LOW_WATERMARK);
+        .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK);
 
     // Create the ChangeStreamMutation through the ChangeStreamRecordBuilder.
     changeStreamRecordBuilder.startUserMutation(
@@ -369,7 +372,7 @@ public void multipleDifferentModsTest() {
                 100L,
                 ByteString.copyFromUtf8("chunked-value"))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK);
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK);
 
     // Create the ChangeStreamMutation through the ChangeStreamRecordBuilder.
     changeStreamRecordBuilder.startUserMutation(
@@ -418,7 +421,7 @@ public void resetTest() {
                 ByteString.copyFromUtf8("key"), "fake-source-cluster-id", FAKE_COMMIT_TIMESTAMP, 0)
             .deleteFamily("fake-family")
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
     changeStreamRecordBuilder.startUserMutation(
         ByteString.copyFromUtf8("key"), "fake-source-cluster-id", FAKE_COMMIT_TIMESTAMP, 0);
@@ -438,7 +441,7 @@ public void resetTest() {
                 100L,
                 ByteString.copyFromUtf8("fake-value1-value2"))
             .setToken("fake-token")
-            .setEstimatedLowWatermark(FAKE_LOW_WATERMARK)
+            .setEstimatedLowWatermarkTime(FAKE_LOW_WATERMARK)
             .build();
 
     changeStreamRecordBuilder.startUserMutation(
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQueryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQueryTest.java
index 699f60a8d1..13e1bcb915 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQueryTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadChangeStreamQueryTest.java
@@ -34,6 +34,7 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.time.Instant;
 import java.util.Collections;
 import org.junit.Before;
 import org.junit.Rule;
@@ -41,7 +42,6 @@
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class ReadChangeStreamQueryTest {
@@ -152,8 +152,7 @@ public void endTimeTest() {
   @Test
   public void heartbeatDurationTest() {
     ReadChangeStreamQuery query =
-        ReadChangeStreamQuery.create(TABLE_ID)
-            .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(5));
+        ReadChangeStreamQuery.create(TABLE_ID).heartbeatDuration(java.time.Duration.ofSeconds(5));
 
     Builder expectedProto =
         expectedProtoBuilder().setHeartbeatDuration(Duration.newBuilder().setSeconds(5).build());
@@ -232,7 +231,7 @@ public void serializationTest() throws IOException, ClassNotFoundException {
             .streamPartition("simple-begin", "simple-end")
             .continuationTokens(Collections.singletonList(token))
             .endTime(FAKE_END_TIME)
-            .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(5));
+            .heartbeatDuration(java.time.Duration.ofSeconds(5));
 
     ByteArrayOutputStream bos = new ByteArrayOutputStream();
     ObjectOutputStream oos = new ObjectOutputStream(bos);
@@ -302,7 +301,7 @@ public void testEquality() {
             .streamPartition("simple-begin", "simple-end")
             .startTime(FAKE_START_TIME)
             .endTime(FAKE_END_TIME)
-            .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(5));
+            .heartbeatDuration(java.time.Duration.ofSeconds(5));
 
     // ReadChangeStreamQuery#toProto should not change the ReadChangeStreamQuery instance state
     request.toProto(requestContext);
@@ -312,7 +311,7 @@ public void testEquality() {
                 .streamPartition("simple-begin", "simple-end")
                 .startTime(FAKE_START_TIME)
                 .endTime(FAKE_END_TIME)
-                .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(5)));
+                .heartbeatDuration(java.time.Duration.ofSeconds(5)));
 
     assertThat(ReadChangeStreamQuery.create(TABLE_ID).streamPartition("begin-1", "end-1"))
         .isNotEqualTo(ReadChangeStreamQuery.create(TABLE_ID).streamPartition("begin-2", "end-1"));
@@ -324,10 +323,10 @@ public void testEquality() {
             ReadChangeStreamQuery.create(TABLE_ID).endTime(Instant.ofEpochSecond(1L, 1001L)));
     assertThat(
             ReadChangeStreamQuery.create(TABLE_ID)
-                .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(5)))
+                .heartbeatDuration(java.time.Duration.ofSeconds(5)))
         .isNotEqualTo(
             ReadChangeStreamQuery.create(TABLE_ID)
-                .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(6)));
+                .heartbeatDuration(java.time.Duration.ofSeconds(6)));
   }
 
   @Test
@@ -350,7 +349,7 @@ public void testClone() {
             .streamPartition("begin", "end")
             .continuationTokens(Collections.singletonList(token))
             .endTime(FAKE_END_TIME)
-            .heartbeatDuration(org.threeten.bp.Duration.ofSeconds(5));
+            .heartbeatDuration(java.time.Duration.ofSeconds(5));
     ReadChangeStreamRequest request =
         ReadChangeStreamRequest.newBuilder()
             .setTableName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/sql/StatementTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/sql/StatementTest.java
index cb19a7fde9..6d4765230e 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/sql/StatementTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/sql/StatementTest.java
@@ -41,12 +41,12 @@
 import com.google.cloud.Date;
 import com.google.cloud.bigtable.data.v2.internal.RequestContext;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class StatementTest {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingCallableTest.java
index f2fe77725d..652049b266 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingCallableTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RateLimitingCallableTest.java
@@ -36,12 +36,12 @@
 import com.google.protobuf.Duration;
 import com.google.rpc.Code;
 import com.google.rpc.Status;
+import java.time.Instant;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
-import org.threeten.bp.Instant;
 
 @RunWith(JUnit4.class)
 public class RateLimitingCallableTest {
@@ -72,7 +72,7 @@ public void testUpdateRate() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setLimiterEnabled(true);
 
-    Instant earlier = Instant.now().minus(org.threeten.bp.Duration.ofHours(1));
+    Instant earlier = Instant.now().minus(java.time.Duration.ofHours(1));
 
     // Make sure rate will be updated.
     callableToTest.getNextRateUpdateTime().set(earlier);
@@ -105,7 +105,7 @@ public void testNoRateLimitInfoDoesNotUpdateRate() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setLimiterEnabled(true);
 
-    Instant earlier = Instant.now().minus(org.threeten.bp.Duration.ofHours(1));
+    Instant earlier = Instant.now().minus(java.time.Duration.ofHours(1));
 
     // Make sure rate will be updated.
     callableToTest.getNextRateUpdateTime().set(earlier);
@@ -131,7 +131,7 @@ public void testInvalidRateLimitInfoDoesNotUpdateRate() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setLimiterEnabled(true);
 
-    Instant earlier = Instant.now().minus(org.threeten.bp.Duration.ofHours(1));
+    Instant earlier = Instant.now().minus(java.time.Duration.ofHours(1));
 
     // make sure QPS will be updated
     callableToTest.getNextRateUpdateTime().set(earlier);
@@ -166,7 +166,7 @@ public void testMissingRateLimitInfoFactorDoesNotUpdateRate() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setLimiterEnabled(true);
 
-    Instant earlier = Instant.now().minus(org.threeten.bp.Duration.ofHours(1));
+    Instant earlier = Instant.now().minus(java.time.Duration.ofHours(1));
 
     // Make sure rate can be updated.
     callableToTest.getNextRateUpdateTime().set(earlier);
@@ -199,7 +199,7 @@ public void testNoUpdateBeforeAllowedTime() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setLimiterEnabled(true);
 
-    Instant later = Instant.now().plus(org.threeten.bp.Duration.ofHours(1));
+    Instant later = Instant.now().plus(java.time.Duration.ofHours(1));
     // Make sure rate will not be updated.
     callableToTest.getNextRateUpdateTime().set(later);
     double oldQps = callableToTest.getCurrentRate();
@@ -232,7 +232,7 @@ public void testDoesNotDisableBeforeAllowedTime() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setLimiterEnabled(true);
 
-    Instant later = Instant.now().plus(org.threeten.bp.Duration.ofHours(1));
+    Instant later = Instant.now().plus(java.time.Duration.ofHours(1));
     // Make sure limiter will not be disabled.
     callableToTest.getNextRateUpdateTime().set(later);
     double oldQps = callableToTest.getCurrentRate();
@@ -257,7 +257,7 @@ public void testEnableWithinPeriodDoesNotUpdateRate() throws Exception {
     callableToTest.call(request, responseObserver, context);
     callableToTest.setRate(1.5);
 
-    Instant later = Instant.now().plus(org.threeten.bp.Duration.ofHours(1));
+    Instant later = Instant.now().plus(java.time.Duration.ofHours(1));
     // Even though the rate update time is far in the future, enable is always allowed.
     callableToTest.getNextRateUpdateTime().set(later);
     double oldQps = callableToTest.getCurrentRate();
@@ -289,7 +289,7 @@ public void testEnableWithinPeriodDoesNotUpdateRate() throws Exception {
   public void testErrorInfoLowerQPS() throws Exception {
     callableToTest.call(request, responseObserver, context);
 
-    Instant earlier = Instant.now().minus(org.threeten.bp.Duration.ofHours(1));
+    Instant earlier = Instant.now().minus(java.time.Duration.ofHours(1));
 
     // make sure QPS will be updated
     callableToTest.getNextRateUpdateTime().set(earlier);
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamRecordMergingCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamRecordMergingCallableTest.java
index f0939fb0cf..a5201770ee 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamRecordMergingCallableTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/changestream/ChangeStreamRecordMergingCallableTest.java
@@ -85,6 +85,11 @@ public void heartbeatTest() {
             Instant.ofEpochSecond(
                 heartbeatProto.getEstimatedLowWatermark().getSeconds(),
                 heartbeatProto.getEstimatedLowWatermark().getNanos()));
+    assertThat(heartbeat.getEstimatedLowWatermarkTime())
+        .isEqualTo(
+            java.time.Instant.ofEpochSecond(
+                heartbeatProto.getEstimatedLowWatermark().getSeconds(),
+                heartbeatProto.getEstimatedLowWatermark().getNanos()));
   }
 
   @Test
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporterTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporterTest.java
index 657db7d8ae..e471b19a20 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporterTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporterTest.java
@@ -37,6 +37,7 @@
 import com.google.api.gax.rpc.UnaryCallable;
 import com.google.cloud.monitoring.v3.MetricServiceClient;
 import com.google.cloud.monitoring.v3.stub.MetricServiceStub;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.monitoring.v3.CreateTimeSeriesRequest;
@@ -95,7 +96,7 @@ public void setUp() {
 
     exporter =
         new BigtableCloudMonitoringExporter(
-            fakeMetricServiceClient, /* applicationResource= */ null, taskId);
+            fakeMetricServiceClient, /* applicationResource= */ Suppliers.ofInstance(null), taskId);
 
     attributes =
         Attributes.builder()
@@ -308,11 +309,12 @@ public void testTimeSeriesForMetricWithGceOrGkeResource() {
     BigtableCloudMonitoringExporter exporter =
         new BigtableCloudMonitoringExporter(
             fakeMetricServiceClient,
-            MonitoredResource.newBuilder()
-                .setType("gce-instance")
-                .putLabels("some-gce-key", "some-gce-value")
-                .putLabels("project_id", gceProjectId)
-                .build(),
+            Suppliers.ofInstance(
+                MonitoredResource.newBuilder()
+                    .setType("gce-instance")
+                    .putLabels("some-gce-key", "some-gce-value")
+                    .putLabels("project_id", gceProjectId)
+                    .build()),
             taskId);
     ArgumentCaptor<CreateTimeSeriesRequest> argumentCaptor =
         ArgumentCaptor.forClass(CreateTimeSeriesRequest.class);
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracerTest.java
index cb0916ad28..71a4728f9f 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracerTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracerTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.cloud.bigtable.data.v2.stub.metrics;
 
+import static com.google.api.gax.util.TimeConversionUtils.toThreetenDuration;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -39,7 +40,6 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
-import org.threeten.bp.Duration;
 
 @RunWith(JUnit4.class)
 public class CompositeTracerTest {
@@ -148,12 +148,24 @@ public void testAttemptCancelled() {
   @Test
   public void testAttemptFailed() {
     RuntimeException error = new RuntimeException();
-    Duration delay = Duration.ofMillis(10);
-    compositeTracer.attemptFailed(error, delay);
-    verify(child1, times(1)).attemptFailed(error, delay);
-    verify(child2, times(1)).attemptFailed(error, delay);
-    verify(child3, times(1)).attemptFailed(error, delay);
-    verify(child4, times(1)).attemptFailed(error, delay);
+    java.time.Duration delay = java.time.Duration.ofMillis(10);
+    compositeTracer.attemptFailed(error, toThreetenDuration(delay));
+    // the implementation of CompositeTracer.attemptFailed delegates to attemptFailedDuration.
+    verify(child1, times(1)).attemptFailedDuration(error, delay);
+    verify(child2, times(1)).attemptFailedDuration(error, delay);
+    verify(child3, times(1)).attemptFailedDuration(error, delay);
+    verify(child4, times(1)).attemptFailedDuration(error, delay);
+  }
+
+  @Test
+  public void testAttemptFailedDuration() {
+    RuntimeException error = new RuntimeException();
+    java.time.Duration delay = java.time.Duration.ofMillis(10);
+    compositeTracer.attemptFailedDuration(error, delay);
+    verify(child1, times(1)).attemptFailedDuration(error, delay);
+    verify(child2, times(1)).attemptFailedDuration(error, delay);
+    verify(child3, times(1)).attemptFailedDuration(error, delay);
+    verify(child4, times(1)).attemptFailedDuration(error, delay);
   }
 
   @Test
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java
index 6dd1ff9bd0..60ec5193e4 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java
@@ -41,6 +41,7 @@
 import com.google.common.collect.Lists;
 import com.google.protobuf.ByteString;
 import com.google.rpc.Status;
+import java.time.Duration;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -49,7 +50,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
-import org.threeten.bp.Duration;
 
 @RunWith(JUnit4.class)
 public class MutateRowsAttemptCallableTest {
@@ -140,7 +140,7 @@ public void missingEntry() throws Exception {
   @Test
   public void testNoRpcTimeout() {
     parentFuture.timedAttemptSettings =
-        parentFuture.timedAttemptSettings.toBuilder().setRpcTimeout(Duration.ZERO).build();
+        parentFuture.timedAttemptSettings.toBuilder().setRpcTimeoutDuration(Duration.ZERO).build();
 
     MutateRowsRequest request =
         MutateRowsRequest.newBuilder().addEntries(Entry.getDefaultInstance()).build();
@@ -405,12 +405,13 @@ static class MockRetryingFuture extends AbstractApiFuture<MutateRowsAttemptResul
     MockRetryingFuture(Duration totalTimeout) {
       this.timedAttemptSettings =
           TimedAttemptSettings.newBuilder()
-              .setRpcTimeout(Duration.ofSeconds(1))
-              .setRetryDelay(Duration.ZERO)
-              .setRandomizedRetryDelay(Duration.ZERO)
+              .setRpcTimeoutDuration(Duration.ofSeconds(1))
+              .setRetryDelayDuration(Duration.ZERO)
+              .setRandomizedRetryDelayDuration(Duration.ZERO)
               .setAttemptCount(0)
               .setFirstAttemptStartTimeNanos(0)
-              .setGlobalSettings(RetrySettings.newBuilder().setTotalTimeout(totalTimeout).build())
+              .setGlobalSettings(
+                  RetrySettings.newBuilder().setTotalTimeoutDuration(totalTimeout).build())
               .build();
     }
 
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java
index 1ddac33720..7c6d3df881 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java
@@ -44,6 +44,7 @@
 import io.grpc.StatusRuntimeException;
 import io.grpc.stub.StreamObserver;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.concurrent.TimeUnit;
@@ -52,7 +53,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.threeten.bp.Duration;
 
 @RunWith(JUnit4.class)
 public class ExecuteQueryCallableTest {
@@ -158,8 +158,8 @@ public void testExecuteQueryRequestsRespectDeadline() throws IOException {
         .executeQuerySettings()
         .setRetrySettings(
             RetrySettings.newBuilder()
-                .setInitialRpcTimeout(Duration.ofMillis(10))
-                .setMaxRpcTimeout(Duration.ofMillis(10))
+                .setInitialRpcTimeoutDuration(Duration.ofMillis(10))
+                .setMaxRpcTimeoutDuration(Duration.ofMillis(10))
                 .build());
 
     try (EnhancedBigtableStub overrideDeadline =
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/PrefixGenerator.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/PrefixGenerator.java
index c19d5a82b7..6e173d6d78 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/PrefixGenerator.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/PrefixGenerator.java
@@ -15,13 +15,13 @@
  */
 package com.google.cloud.bigtable.test_helpers.env;
 
+import java.time.Instant;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Logger;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
-import org.threeten.bp.Instant;
 
 public class PrefixGenerator implements TestRule {
   private static final Logger LOGGER = Logger.getLogger(TestEnvRule.class.getName());
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/TestEnvRule.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/TestEnvRule.java
index b6e4651c6b..3e77b00de4 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/TestEnvRule.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/test_helpers/env/TestEnvRule.java
@@ -24,9 +24,13 @@
 import com.google.cloud.bigtable.admin.v2.models.AppProfile;
 import com.google.cloud.bigtable.admin.v2.models.Cluster;
 import com.google.cloud.bigtable.admin.v2.models.Instance;
+import com.google.cloud.bigtable.admin.v2.models.Table;
 import com.google.cloud.bigtable.admin.v2.models.UpdateAuthorizedViewRequest;
+import com.google.cloud.bigtable.admin.v2.models.UpdateTableRequest;
 import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -37,8 +41,6 @@
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
-import org.threeten.bp.Instant;
-import org.threeten.bp.temporal.ChronoUnit;
 
 /**
  * JUnit rule to start and stop the target test environment.
@@ -178,8 +180,19 @@ private void cleanupStaleTables(String stalePrefix) {
   }
 
   private void prepTableForDelete(String tableId) {
-    // Unprotected views
     if (!(env() instanceof EmulatorEnv)) {
+      // unprotect table
+      Table table = env().getTableAdminClient().getTable(tableId);
+      if (table.isDeletionProtected() || table.getChangeStreamRetention() != null) {
+        env()
+            .getTableAdminClient()
+            .updateTable(
+                UpdateTableRequest.of(tableId)
+                    .setDeletionProtection(false)
+                    .disableChangeStreamRetention());
+      }
+
+      // Unprotected views
       for (String viewId : env().getTableAdminClient().listAuthorizedViews(tableId)) {
         try {
           env()
diff --git a/grpc-google-cloud-bigtable-admin-v2/pom.xml b/grpc-google-cloud-bigtable-admin-v2/pom.xml
index 4b8cc2c638..4894f1d527 100644
--- a/grpc-google-cloud-bigtable-admin-v2/pom.xml
+++ b/grpc-google-cloud-bigtable-admin-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-bigtable-admin-v2</artifactId>
-  <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-bigtable-admin-v2:current} -->
+  <version>2.50.0</version><!-- {x-version-update:grpc-google-cloud-bigtable-admin-v2:current} -->
   <name>grpc-google-cloud-bigtable-admin-v2</name>
   <description>GRPC library for grpc-google-cloud-bigtable-admin-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-parent</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
 
   <dependencyManagement>
@@ -18,14 +18,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/grpc-google-cloud-bigtable-v2/pom.xml b/grpc-google-cloud-bigtable-v2/pom.xml
index 494d67ff67..2d54278234 100644
--- a/grpc-google-cloud-bigtable-v2/pom.xml
+++ b/grpc-google-cloud-bigtable-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-bigtable-v2</artifactId>
-  <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-bigtable-v2:current} -->
+  <version>2.50.0</version><!-- {x-version-update:grpc-google-cloud-bigtable-v2:current} -->
   <name>grpc-google-cloud-bigtable-v2</name>
   <description>GRPC library for grpc-google-cloud-bigtable-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-parent</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
 
   <dependencyManagement>
@@ -18,14 +18,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/pom.xml b/pom.xml
index d4fcfe8dbb..bab3bd00f1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     <artifactId>google-cloud-bigtable-parent</artifactId>
     <packaging>pom</packaging>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
     <name>Google Cloud Bigtable Parent</name>
     <url>https://github.com/googleapis/java-bigtable</url>
     <description>
@@ -14,7 +14,7 @@
     <parent>
         <groupId>com.google.cloud</groupId>
         <artifactId>sdk-platform-java-config</artifactId>
-        <version>3.40.0</version>
+        <version>3.41.0</version>
         <relativePath/>
     </parent>
 
@@ -153,27 +153,27 @@
             <dependency>
                 <groupId>com.google.api.grpc</groupId>
                 <artifactId>proto-google-cloud-bigtable-v2</artifactId>
-                <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-bigtable-v2:current} -->
+                <version>2.50.0</version><!-- {x-version-update:proto-google-cloud-bigtable-v2:current} -->
             </dependency>
             <dependency>
                 <groupId>com.google.api.grpc</groupId>
                 <artifactId>proto-google-cloud-bigtable-admin-v2</artifactId>
-                <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-bigtable-admin-v2:current} -->
+                <version>2.50.0</version><!-- {x-version-update:proto-google-cloud-bigtable-admin-v2:current} -->
             </dependency>
             <dependency>
                 <groupId>com.google.api.grpc</groupId>
                 <artifactId>grpc-google-cloud-bigtable-v2</artifactId>
-                <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-bigtable-v2:current} -->
+                <version>2.50.0</version><!-- {x-version-update:grpc-google-cloud-bigtable-v2:current} -->
             </dependency>
             <dependency>
                 <groupId>com.google.api.grpc</groupId>
                 <artifactId>grpc-google-cloud-bigtable-admin-v2</artifactId>
-                <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-bigtable-admin-v2:current} -->
+                <version>2.50.0</version><!-- {x-version-update:grpc-google-cloud-bigtable-admin-v2:current} -->
             </dependency>
             <dependency>
                 <groupId>com.google.cloud</groupId>
                 <artifactId>google-cloud-bigtable</artifactId>
-                <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+                <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
             </dependency>
             <!-- Test Deps in alphabetical order -->
             <dependency>
diff --git a/proto-google-cloud-bigtable-admin-v2/pom.xml b/proto-google-cloud-bigtable-admin-v2/pom.xml
index 2da529ade4..defb843298 100644
--- a/proto-google-cloud-bigtable-admin-v2/pom.xml
+++ b/proto-google-cloud-bigtable-admin-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-bigtable-admin-v2</artifactId>
-  <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-bigtable-admin-v2:current} -->
+  <version>2.50.0</version><!-- {x-version-update:proto-google-cloud-bigtable-admin-v2:current} -->
   <name>proto-google-cloud-bigtable-admin-v2</name>
   <description>PROTO library for proto-google-cloud-bigtable-admin-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-parent</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
 
   <dependencyManagement>
@@ -18,14 +18,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/proto-google-cloud-bigtable-v2/pom.xml b/proto-google-cloud-bigtable-v2/pom.xml
index 51416f8938..efe2dd1bcb 100644
--- a/proto-google-cloud-bigtable-v2/pom.xml
+++ b/proto-google-cloud-bigtable-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-bigtable-v2</artifactId>
-  <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-bigtable-v2:current} -->
+  <version>2.50.0</version><!-- {x-version-update:proto-google-cloud-bigtable-v2:current} -->
   <name>proto-google-cloud-bigtable-v2</name>
   <description>PROTO library for proto-google-cloud-bigtable-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-bigtable-parent</artifactId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
 
   <dependencyManagement>
@@ -18,14 +18,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-deps-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-bigtable-bom</artifactId>
-        <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+        <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 2becdec592..621f77096e 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-bigtable</artifactId>
-      <version>2.49.1-SNAPSHOT</version>
+      <version>2.50.0</version>
     </dependency>
   <!-- {x-version-update-end} -->
 
diff --git a/test-proxy/README.md b/test-proxy/README.md
index 18778ba8c3..f87a3374ca 100644
--- a/test-proxy/README.md
+++ b/test-proxy/README.md
@@ -1,6 +1,6 @@
 # CBT Java Test Proxy
 
-The CBT test proxy is intended for running confromance tests for Cloug Bigtable Java Client.
+The CBT test proxy is intended for running conformance tests for Cloud Bigtable Java Client.
 
 ## Set up
 
diff --git a/test-proxy/pom.xml b/test-proxy/pom.xml
index 8ac8cfb307..e2f23bba5f 100644
--- a/test-proxy/pom.xml
+++ b/test-proxy/pom.xml
@@ -12,11 +12,11 @@
   <parent>
     <artifactId>google-cloud-bigtable-parent</artifactId>
     <groupId>com.google.cloud</groupId>
-    <version>2.49.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <version>2.50.0</version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </parent>
 
   <properties>
-    <bigtable.client.version>2.49.1-SNAPSHOT</bigtable.client.version><!-- {x-version-update:google-cloud-bigtable:current} -->
+    <bigtable.client.version>2.50.0</bigtable.client.version><!-- {x-version-update:google-cloud-bigtable:current} -->
   </properties>
 
   <dependencyManagement>
diff --git a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java
index 16b5c8257c..86eae14dee 100644
--- a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java
+++ b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java
@@ -60,6 +60,7 @@
 import java.io.ByteArrayInputStream;
 import java.io.Closeable;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -70,7 +71,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import org.threeten.bp.Duration;
 
 /** Java implementation of the CBT test proxy. Used to test the Java CBT client. */
 public class CbtTestProxy extends CloudBigtableV2TestProxyImplBase implements Closeable {
@@ -129,15 +129,15 @@ private static BigtableDataSettings.Builder overrideTimeoutSetting(
   }
 
   private static void updateTimeout(RetrySettings.Builder settings, Duration newTimeout) {
-    Duration rpcTimeout = settings.getInitialRpcTimeout();
+    Duration rpcTimeout = settings.getInitialRpcTimeoutDuration();
 
     // TODO: this should happen in gax
     // Clamp the rpcTimeout to the overall timeout
     if (rpcTimeout != null && rpcTimeout.compareTo(newTimeout) > 0) {
-      settings.setInitialRpcTimeout(newTimeout).setMaxRpcTimeout(newTimeout);
+      settings.setInitialRpcTimeoutDuration(newTimeout).setMaxRpcTimeoutDuration(newTimeout);
     }
 
-    settings.setTotalTimeout(newTimeout);
+    settings.setTotalTimeoutDuration(newTimeout);
   }
 
   /** Helper method to get a client object by its id. */
diff --git a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/ResultSetSerializer.java b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/ResultSetSerializer.java
index c138c82a6b..7400986b6e 100644
--- a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/ResultSetSerializer.java
+++ b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/ResultSetSerializer.java
@@ -33,9 +33,9 @@
 import com.google.cloud.bigtable.data.v2.models.sql.SqlType;
 import com.google.cloud.bigtable.data.v2.models.sql.StructReader;
 import com.google.protobuf.ByteString;
+import java.time.Instant;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import org.threeten.bp.Instant;
 
 public class ResultSetSerializer {
   public static ExecuteQueryResult toExecuteQueryResult(ResultSet resultSet)
diff --git a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/StatementDeserializer.java b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/StatementDeserializer.java
index ae3b50aa7f..4eb5f47e3a 100644
--- a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/StatementDeserializer.java
+++ b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/StatementDeserializer.java
@@ -21,10 +21,10 @@
 import com.google.cloud.bigtable.data.v2.models.sql.SqlType;
 import com.google.cloud.bigtable.data.v2.models.sql.Statement;
 import com.google.protobuf.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import org.threeten.bp.Instant;
 
 public class StatementDeserializer {
 
diff --git a/versions.txt b/versions.txt
index 36c026c00a..f21d580d34 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,10 +1,10 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-bigtable:2.49.0:2.49.1-SNAPSHOT
-grpc-google-cloud-bigtable-admin-v2:2.49.0:2.49.1-SNAPSHOT
-grpc-google-cloud-bigtable-v2:2.49.0:2.49.1-SNAPSHOT
-proto-google-cloud-bigtable-admin-v2:2.49.0:2.49.1-SNAPSHOT
-proto-google-cloud-bigtable-v2:2.49.0:2.49.1-SNAPSHOT
-google-cloud-bigtable-emulator:0.186.0:0.186.1-SNAPSHOT
-google-cloud-bigtable-emulator-core:0.186.0:0.186.1-SNAPSHOT
+google-cloud-bigtable:2.50.0:2.50.0
+grpc-google-cloud-bigtable-admin-v2:2.50.0:2.50.0
+grpc-google-cloud-bigtable-v2:2.50.0:2.50.0
+proto-google-cloud-bigtable-admin-v2:2.50.0:2.50.0
+proto-google-cloud-bigtable-v2:2.50.0:2.50.0
+google-cloud-bigtable-emulator:0.187.0:0.187.0
+google-cloud-bigtable-emulator-core:0.187.0:0.187.0