From 30a5635d425fde435e1e40f87f4ea62a0141feec Mon Sep 17 00:00:00 2001
From: Surbhi Garg <gargsurbhi@google.com>
Date: Fri, 29 May 2026 17:38:44 +0530
Subject: [PATCH 1/7] feat(spanner): wrap OTel background maintenance
 setInterval in ROOT_CONTEXT to isolate and prevent memory leaks

---
 handwritten/spanner/src/multiplexed-session.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/handwritten/spanner/src/multiplexed-session.ts b/handwritten/spanner/src/multiplexed-session.ts
index fdcc6675ceb6..8a11a8f287df 100644
--- a/handwritten/spanner/src/multiplexed-session.ts
+++ b/handwritten/spanner/src/multiplexed-session.ts
@@ -15,6 +15,7 @@
  */
 
 import {EventEmitter} from 'events';
+import {context, ROOT_CONTEXT} from '@opentelemetry/api';
 import {Database} from './database';
 import {Session} from './session';
 import {GetSessionCallback} from './session-factory';
@@ -177,8 +178,10 @@ export class MultiplexedSession
       clearInterval(this._refreshHandle);
     }
     const refreshRate = this.refreshRate! * 24 * 60 * 60000;
-    this._refreshHandle = setInterval(async () => {
-      await this._createSession().catch(() => {});
+    this._refreshHandle = setInterval(() => {
+      context.with(ROOT_CONTEXT, async () => {
+        await this._createSession().catch(() => {});
+      });
     }, refreshRate);
 
     // Unreference the timer so it does not prevent the Node.js process from exiting.

From 75bb97749180e652eccb5fd1d8a0046a88e758e7 Mon Sep 17 00:00:00 2001
From: Surbhi Garg <gargsurbhi@google.com>
Date: Fri, 29 May 2026 17:47:50 +0530
Subject: [PATCH 2/7] feat(spanner): wrap legacy SessionPool setInterval pings
 and evictions in ROOT_CONTEXT to isolate and prevent memory leaks

---
 handwritten/spanner/src/multiplexed-session.ts | 4 ++--
 handwritten/spanner/src/session-pool.ts        | 9 +++++++--
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/handwritten/spanner/src/multiplexed-session.ts b/handwritten/spanner/src/multiplexed-session.ts
index 8a11a8f287df..1f60ab65a297 100644
--- a/handwritten/spanner/src/multiplexed-session.ts
+++ b/handwritten/spanner/src/multiplexed-session.ts
@@ -179,8 +179,8 @@ export class MultiplexedSession
     }
     const refreshRate = this.refreshRate! * 24 * 60 * 60000;
     this._refreshHandle = setInterval(() => {
-      context.with(ROOT_CONTEXT, async () => {
-        await this._createSession().catch(() => {});
+      context.with(ROOT_CONTEXT, () => {
+        this._createSession().catch(() => { });
       });
     }, refreshRate);
 
diff --git a/handwritten/spanner/src/session-pool.ts b/handwritten/spanner/src/session-pool.ts
index 3c80e5bdceb2..662ac2a1957f 100644
--- a/handwritten/spanner/src/session-pool.ts
+++ b/handwritten/spanner/src/session-pool.ts
@@ -22,6 +22,7 @@ import {Session} from './session';
 import {Transaction} from './transaction';
 import {NormalCallback} from './common';
 import {GoogleError, grpc, ServiceError} from 'google-gax';
+import {context, ROOT_CONTEXT} from '@opentelemetry/api';
 import trace = require('stack-trace');
 import {
   ObservabilityOptions,
@@ -1061,12 +1062,16 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
   _startHouseKeeping(): void {
     const evictRate = this.options.idlesAfter! * 60000;
 
-    this._evictHandle = setInterval(() => this._evictIdleSessions(), evictRate);
+    this._evictHandle = setInterval(() => {
+      context.with(ROOT_CONTEXT, () => this._evictIdleSessions());
+    }, evictRate);
     this._evictHandle.unref();
 
     const pingRate = this.options.keepAlive! * 60000;
 
-    this._pingHandle = setInterval(() => this._pingIdleSessions(), pingRate);
+    this._pingHandle = setInterval(() => {
+      context.with(ROOT_CONTEXT, () => this._pingIdleSessions());
+    }, pingRate);
     this._pingHandle.unref();
   }
 

From 2f084c41a0e4dfedb889c93578658d264f394d09 Mon Sep 17 00:00:00 2001
From: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 13:11:19 +0000
Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20po?=
 =?UTF-8?q?st-processor?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---
 handwritten/spanner/src/helper.ts             |  7 +++++-
 .../spanner/src/multiplexed-session.ts        |  2 +-
 handwritten/spanner/system-test/spanner.ts    | 24 +++++++++++++++----
 3 files changed, 27 insertions(+), 6 deletions(-)

diff --git a/handwritten/spanner/src/helper.ts b/handwritten/spanner/src/helper.ts
index e50802bf5c1e..42c67d53e05b 100644
--- a/handwritten/spanner/src/helper.ts
+++ b/handwritten/spanner/src/helper.ts
@@ -283,5 +283,10 @@ export function isError(value: any): boolean {
  * @returns {Boolean} `true` if the value is a UUID, otherwise `false`.
  */
 export function isUuid(value: any): boolean {
-  return typeof value === 'string' && /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i.test(value);
+  return (
+    typeof value === 'string' &&
+    /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i.test(
+      value,
+    )
+  );
 }
diff --git a/handwritten/spanner/src/multiplexed-session.ts b/handwritten/spanner/src/multiplexed-session.ts
index 1f60ab65a297..fee5b4db9427 100644
--- a/handwritten/spanner/src/multiplexed-session.ts
+++ b/handwritten/spanner/src/multiplexed-session.ts
@@ -180,7 +180,7 @@ export class MultiplexedSession
     const refreshRate = this.refreshRate! * 24 * 60 * 60000;
     this._refreshHandle = setInterval(() => {
       context.with(ROOT_CONTEXT, () => {
-        this._createSession().catch(() => { });
+        this._createSession().catch(() => {});
       });
     }, refreshRate);
 
diff --git a/handwritten/spanner/system-test/spanner.ts b/handwritten/spanner/system-test/spanner.ts
index 7031a7fe49cd..4b774fcf85e3 100644
--- a/handwritten/spanner/system-test/spanner.ts
+++ b/handwritten/spanner/system-test/spanner.ts
@@ -933,7 +933,11 @@ describe('Spanner', () => {
       });
 
       it('GOOGLE_STANDARD_SQL should write uuid array values', async () => {
-        const values = [crypto.randomUUID(), crypto.randomUUID(), crypto.randomUUID()];
+        const values = [
+          crypto.randomUUID(),
+          crypto.randomUUID(),
+          crypto.randomUUID(),
+        ];
         const {row} = await insert(
           {UUIDArray: values},
           Spanner.GOOGLE_STANDARD_SQL,
@@ -942,7 +946,11 @@ describe('Spanner', () => {
       });
 
       it.skip('POSTGRESQL should write uuid array values', async () => {
-        const values = [crypto.randomUUID(), crypto.randomUUID(), crypto.randomUUID()];
+        const values = [
+          crypto.randomUUID(),
+          crypto.randomUUID(),
+          crypto.randomUUID(),
+        ];
         const {row} = await insert({UUIDArray: values}, Spanner.POSTGRESQL);
         assert.deepStrictEqual(row.toJSON().UUIDArray, values);
       });
@@ -4674,7 +4682,11 @@ describe('Spanner', () => {
           });
 
           it('GOOGLE_STANDARD_SQL should bind arrays', async () => {
-            const values = [crypto.randomUUID(), crypto.randomUUID(), crypto.randomUUID()];
+            const values = [
+              crypto.randomUUID(),
+              crypto.randomUUID(),
+              crypto.randomUUID(),
+            ];
 
             const query = {
               sql: 'SELECT @v',
@@ -4714,7 +4726,11 @@ describe('Spanner', () => {
           });
 
           it.skip('POSTGRESQL should bind arrays', async () => {
-            const values = [crypto.randomUUID(), crypto.randomUUID(), crypto.randomUUID()];
+            const values = [
+              crypto.randomUUID(),
+              crypto.randomUUID(),
+              crypto.randomUUID(),
+            ];
 
             const query = {
               sql: 'SELECT $1',

From 40011f7d6b838c73b39d2ae8d0182c1e9466f25a Mon Sep 17 00:00:00 2001
From: Surbhi Garg <gargsurbhi@google.com>
Date: Fri, 29 May 2026 18:41:10 +0530
Subject: [PATCH 4/7] refactor(spanner): invoke setInterval inside
 context.with(ROOT_CONTEXT) to prevent OTel AsyncLocalStorage scheduling
 context leaks

---
 .../spanner/src/multiplexed-session.ts        | 25 +++++++++----------
 handwritten/spanner/src/session-pool.ts       | 12 ++++-----
 2 files changed, 18 insertions(+), 19 deletions(-)

diff --git a/handwritten/spanner/src/multiplexed-session.ts b/handwritten/spanner/src/multiplexed-session.ts
index fee5b4db9427..9aee336a1697 100644
--- a/handwritten/spanner/src/multiplexed-session.ts
+++ b/handwritten/spanner/src/multiplexed-session.ts
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-import {EventEmitter} from 'events';
-import {context, ROOT_CONTEXT} from '@opentelemetry/api';
-import {Database} from './database';
-import {Session} from './session';
-import {GetSessionCallback} from './session-factory';
+import { EventEmitter } from 'events';
+import { context, ROOT_CONTEXT } from '@opentelemetry/api';
+import { Database } from './database';
+import { Session } from './session';
+import { GetSessionCallback } from './session-factory';
 import {
   ObservabilityOptions,
   getActiveOrNoopSpan,
@@ -64,8 +64,7 @@ export interface MultiplexedSessionInterface extends EventEmitter {
  */
 export class MultiplexedSession
   extends EventEmitter
-  implements MultiplexedSessionInterface
-{
+  implements MultiplexedSessionInterface {
   database: Database;
   // frequency to create new mux session
   refreshRate: number;
@@ -98,7 +97,7 @@ export class MultiplexedSession
       })
       // Ignore errors here. If this fails, the next user request will
       // automatically trigger a retry via `_getSession`.
-      .catch(err => {});
+      .catch(err => { });
   }
 
   /**
@@ -178,11 +177,11 @@ export class MultiplexedSession
       clearInterval(this._refreshHandle);
     }
     const refreshRate = this.refreshRate! * 24 * 60 * 60000;
-    this._refreshHandle = setInterval(() => {
-      context.with(ROOT_CONTEXT, () => {
-        this._createSession().catch(() => {});
-      });
-    }, refreshRate);
+    this._refreshHandle = context.with(ROOT_CONTEXT, () =>
+      setInterval(() => {
+        this._createSession().catch(() => { });
+      }, refreshRate)
+    );
 
     // Unreference the timer so it does not prevent the Node.js process from exiting.
     // If the application has finished all other work, this background timer shouldn't
diff --git a/handwritten/spanner/src/session-pool.ts b/handwritten/spanner/src/session-pool.ts
index 662ac2a1957f..aa9c49a5fa01 100644
--- a/handwritten/spanner/src/session-pool.ts
+++ b/handwritten/spanner/src/session-pool.ts
@@ -1062,16 +1062,16 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
   _startHouseKeeping(): void {
     const evictRate = this.options.idlesAfter! * 60000;
 
-    this._evictHandle = setInterval(() => {
-      context.with(ROOT_CONTEXT, () => this._evictIdleSessions());
-    }, evictRate);
+    this._evictHandle = context.with(ROOT_CONTEXT, () =>
+      setInterval(() => this._evictIdleSessions(), evictRate)
+    );
     this._evictHandle.unref();
 
     const pingRate = this.options.keepAlive! * 60000;
 
-    this._pingHandle = setInterval(() => {
-      context.with(ROOT_CONTEXT, () => this._pingIdleSessions());
-    }, pingRate);
+    this._pingHandle = context.with(ROOT_CONTEXT, () =>
+      setInterval(() => this._pingIdleSessions(), pingRate)
+    );
     this._pingHandle.unref();
   }
 

From 0589d59b308bac376c2c1c7d5c426bba7c0d75d1 Mon Sep 17 00:00:00 2001
From: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 13:30:43 +0000
Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20po?=
 =?UTF-8?q?st-processor?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---
 .../spanner/src/multiplexed-session.ts        | 19 ++++++++++---------
 handwritten/spanner/src/session-pool.ts       |  4 ++--
 2 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/handwritten/spanner/src/multiplexed-session.ts b/handwritten/spanner/src/multiplexed-session.ts
index 9aee336a1697..fff38e90409d 100644
--- a/handwritten/spanner/src/multiplexed-session.ts
+++ b/handwritten/spanner/src/multiplexed-session.ts
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-import { EventEmitter } from 'events';
-import { context, ROOT_CONTEXT } from '@opentelemetry/api';
-import { Database } from './database';
-import { Session } from './session';
-import { GetSessionCallback } from './session-factory';
+import {EventEmitter} from 'events';
+import {context, ROOT_CONTEXT} from '@opentelemetry/api';
+import {Database} from './database';
+import {Session} from './session';
+import {GetSessionCallback} from './session-factory';
 import {
   ObservabilityOptions,
   getActiveOrNoopSpan,
@@ -64,7 +64,8 @@ export interface MultiplexedSessionInterface extends EventEmitter {
  */
 export class MultiplexedSession
   extends EventEmitter
-  implements MultiplexedSessionInterface {
+  implements MultiplexedSessionInterface
+{
   database: Database;
   // frequency to create new mux session
   refreshRate: number;
@@ -97,7 +98,7 @@ export class MultiplexedSession
       })
       // Ignore errors here. If this fails, the next user request will
       // automatically trigger a retry via `_getSession`.
-      .catch(err => { });
+      .catch(err => {});
   }
 
   /**
@@ -179,8 +180,8 @@ export class MultiplexedSession
     const refreshRate = this.refreshRate! * 24 * 60 * 60000;
     this._refreshHandle = context.with(ROOT_CONTEXT, () =>
       setInterval(() => {
-        this._createSession().catch(() => { });
-      }, refreshRate)
+        this._createSession().catch(() => {});
+      }, refreshRate),
     );
 
     // Unreference the timer so it does not prevent the Node.js process from exiting.
diff --git a/handwritten/spanner/src/session-pool.ts b/handwritten/spanner/src/session-pool.ts
index aa9c49a5fa01..bed5e7c322da 100644
--- a/handwritten/spanner/src/session-pool.ts
+++ b/handwritten/spanner/src/session-pool.ts
@@ -1063,14 +1063,14 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
     const evictRate = this.options.idlesAfter! * 60000;
 
     this._evictHandle = context.with(ROOT_CONTEXT, () =>
-      setInterval(() => this._evictIdleSessions(), evictRate)
+      setInterval(() => this._evictIdleSessions(), evictRate),
     );
     this._evictHandle.unref();
 
     const pingRate = this.options.keepAlive! * 60000;
 
     this._pingHandle = context.with(ROOT_CONTEXT, () =>
-      setInterval(() => this._pingIdleSessions(), pingRate)
+      setInterval(() => this._pingIdleSessions(), pingRate),
     );
     this._pingHandle.unref();
   }

From ad478dfe9b82c03c7be244002d4ff03ad51d0381 Mon Sep 17 00:00:00 2001
From: Surbhi Garg <gargsurbhi@google.com>
Date: Fri, 29 May 2026 20:47:39 +0530
Subject: [PATCH 6/7] test(spanner): add unit test programmatically verifying
 ROOT_CONTEXT scheduling isolation for background timers

---
 .../spanner/observability-test/spanner.ts     | 46 +++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/handwritten/spanner/observability-test/spanner.ts b/handwritten/spanner/observability-test/spanner.ts
index d298c7d7dd3d..555b14bda36e 100644
--- a/handwritten/spanner/observability-test/spanner.ts
+++ b/handwritten/spanner/observability-test/spanner.ts
@@ -18,6 +18,7 @@ import * as assert from 'assert';
 import {grpc} from 'google-gax';
 import {google} from '../protos/protos';
 import {Database, Instance, Spanner} from '../src';
+import {MultiplexedSession} from '../src/multiplexed-session';
 import {MutationSet} from '../src/transaction';
 import protobuf = google.spanner.v1;
 import v1 = google.spanner.v1;
@@ -2024,4 +2025,49 @@ describe('End to end tracing headers', () => {
       txn.end();
     }
   });
+
+  it('should schedule background timers in ROOT_CONTEXT to prevent trace/memory leaks', async () => {
+    const originalSetInterval = global.setInterval;
+    let capturedSchedulingContext: any;
+    
+    // Create an active user span context
+    const otelApi = require('@opentelemetry/api');
+    const userTracer = otelApi.trace.getTracer('test-tracer');
+    const userSpan = userTracer.startSpan('User.API.Request');
+    
+    try {
+      await otelApi.context.with(otelApi.trace.setSpan(otelApi.ROOT_CONTEXT, userSpan), async () => {
+        // Monkey-patch setInterval to capture context exactly at scheduling time
+        (global as any).setInterval = function (callback: any, ms: any) {
+          capturedSchedulingContext = otelApi.context.active();
+          // Return a mock handle
+          return {
+            unref: () => {},
+          };
+        };
+
+        // Trigger the MultiplexedSession _maintain() schedule method
+        // (Accessing private method for targeted unit verification)
+        const mux = new MultiplexedSession({} as any);
+        (mux as any)._maintain();
+      });
+
+      // Verify that scheduling DID NOT capture the active user span context,
+      // but instead securely captured ROOT_CONTEXT
+      assert.strictEqual(
+        otelApi.trace.getSpan(capturedSchedulingContext),
+        undefined,
+        'setInterval MUST be scheduled under ROOT_CONTEXT'
+      );
+      assert.strictEqual(
+        capturedSchedulingContext,
+        otelApi.ROOT_CONTEXT,
+        'setInterval context did not match ROOT_CONTEXT'
+      );
+    } finally {
+      // Clean up global state
+      global.setInterval = originalSetInterval;
+      userSpan.end();
+    }
+  });
 });

From 5f5d1c5964b0c0c3a3e9b8316b8b235f9234f03b Mon Sep 17 00:00:00 2001
From: Surbhi Garg <gargsurbhi@google.com>
Date: Tue, 2 Jun 2026 11:34:10 +0530
Subject: [PATCH 7/7] test(spanner): isolate MetricsTracerFactory cleanup timer
 and add comprehensive OpenTelemetry context isolation tests

---
 .../observability-test/context-isolation.ts   | 196 ++++++++++++++++++
 .../spanner/observability-test/spanner.ts     |  46 ----
 .../src/metrics/metrics-tracer-factory.ts     |  10 +-
 3 files changed, 202 insertions(+), 50 deletions(-)
 create mode 100644 handwritten/spanner/observability-test/context-isolation.ts

diff --git a/handwritten/spanner/observability-test/context-isolation.ts b/handwritten/spanner/observability-test/context-isolation.ts
new file mode 100644
index 000000000000..8304a5927890
--- /dev/null
+++ b/handwritten/spanner/observability-test/context-isolation.ts
@@ -0,0 +1,196 @@
+/*!
+ * Copyright 2026 Google LLC. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as assert from 'assert';
+import {before, after, beforeEach, afterEach, describe, it} from 'mocha';
+import * as sinon from 'sinon';
+import {context, trace} from '@opentelemetry/api';
+import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
+import {AlwaysOnSampler} from '@opentelemetry/sdk-trace-base';
+import {Database} from '../src/database';
+import {SessionPool} from '../src/session-pool';
+import {MultiplexedSession} from '../src/multiplexed-session';
+import {MetricsTracerFactory} from '../src/metrics/metrics-tracer-factory';
+import {
+  ensureInitialContextManagerSet,
+  _resetTracingEnabledForTest,
+} from '../src/instrument';
+
+describe('OpenTelemetry Context Isolation Tests', () => {
+  const sandbox = sinon.createSandbox();
+  let provider: NodeTracerProvider;
+
+  const MOCK_DATABASE = {
+    batchCreateSessions: sandbox.stub().resolves([[]]),
+    databaseRole: 'parent_role',
+    _observabilityOptions: {},
+  } as unknown as Database;
+
+  before(() => {
+    _resetTracingEnabledForTest();
+    ensureInitialContextManagerSet();
+
+    provider = new NodeTracerProvider({
+      sampler: new AlwaysOnSampler(),
+    });
+    provider.register();
+    _resetTracingEnabledForTest();
+  });
+
+  after(async () => {
+    await provider.shutdown();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  describe('SessionPool background housekeeping timers', () => {
+    let sessionPool: SessionPool;
+
+    beforeEach(() => {
+      sessionPool = new SessionPool(MOCK_DATABASE, {
+        min: 0,
+        max: 10,
+        idlesAfter: 10,
+        keepAlive: 30,
+      });
+    });
+
+    afterEach(() => {
+      sessionPool._stopHouseKeeping();
+    });
+
+    it('should schedule evict and keep-alive setInterval calls in ROOT_CONTEXT', () => {
+      const tracer = trace.getTracer('test');
+
+      const setIntervalStub = sandbox
+        .stub(global, 'setInterval')
+        .callsFake(() => {
+          const activeSpan = trace.getSpan(context.active());
+
+          // Assert that the active context is ROOT_CONTEXT (i.e., no active span)
+          assert.strictEqual(
+            activeSpan,
+            undefined,
+            'setInterval scheduling must be isolated within ROOT_CONTEXT and not carry any active request span',
+          );
+          return {
+            unref: () => {},
+          } as unknown as NodeJS.Timeout;
+        });
+
+      // Start an active request context
+      tracer.startActiveSpan('request-span', span => {
+        try {
+          // Start housekeeping under the request context
+          sessionPool._startHouseKeeping();
+        } finally {
+          span.end();
+        }
+      });
+
+      // Verify that both evict and ping intervals were scheduled
+      assert.strictEqual(setIntervalStub.callCount, 2);
+    });
+  });
+
+  describe('MultiplexedSession background maintenance timer', () => {
+    let multiplexedSession: MultiplexedSession;
+
+    beforeEach(() => {
+      multiplexedSession = new MultiplexedSession(MOCK_DATABASE);
+    });
+
+    afterEach(() => {
+      if (multiplexedSession._refreshHandle) {
+        clearInterval(multiplexedSession._refreshHandle);
+      }
+    });
+
+    it('should schedule multiplexed session maintenance setInterval in ROOT_CONTEXT', () => {
+      const tracer = trace.getTracer('test');
+
+      const setIntervalStub = sandbox
+        .stub(global, 'setInterval')
+        .callsFake(() => {
+          const activeSpan = trace.getSpan(context.active());
+
+          // Assert that the active context is ROOT_CONTEXT (i.e., no active span)
+          assert.strictEqual(
+            activeSpan,
+            undefined,
+            'setInterval scheduling must be isolated within ROOT_CONTEXT and not carry any active request span',
+          );
+          return {
+            unref: () => {},
+          } as unknown as NodeJS.Timeout;
+        });
+
+      // Start an active request context
+      tracer.startActiveSpan('request-span', span => {
+        try {
+          // Start maintenance under the request context
+          multiplexedSession._maintain();
+        } finally {
+          span.end();
+        }
+      });
+
+      // Verify that the refresh interval was scheduled
+      assert.strictEqual(setIntervalStub.callCount, 1);
+    });
+  });
+
+  describe('MetricsTracerFactory background cleanup timer', () => {
+    afterEach(async () => {
+      await MetricsTracerFactory.resetInstance();
+    });
+
+    it('should schedule MetricsTracerFactory cleanup setInterval in ROOT_CONTEXT', () => {
+      const tracer = trace.getTracer('test');
+
+      const setIntervalStub = sandbox
+        .stub(global, 'setInterval')
+        .callsFake(() => {
+          const activeSpan = trace.getSpan(context.active());
+
+          // Assert that the active context is ROOT_CONTEXT (i.e., no active span)
+          assert.strictEqual(
+            activeSpan,
+            undefined,
+            'setInterval scheduling must be isolated within ROOT_CONTEXT and not carry any active request span',
+          );
+          return {
+            unref: () => {},
+          } as unknown as NodeJS.Timeout;
+        });
+
+      // Start an active request context
+      tracer.startActiveSpan('request-span', span => {
+        try {
+          // Instantiate the singleton under a request context
+          MetricsTracerFactory.getInstance('mock-project-id');
+        } finally {
+          span.end();
+        }
+      });
+
+      // Verify that the cleanup interval was scheduled
+      assert.strictEqual(setIntervalStub.callCount, 1);
+    });
+  });
+});
diff --git a/handwritten/spanner/observability-test/spanner.ts b/handwritten/spanner/observability-test/spanner.ts
index 555b14bda36e..d298c7d7dd3d 100644
--- a/handwritten/spanner/observability-test/spanner.ts
+++ b/handwritten/spanner/observability-test/spanner.ts
@@ -18,7 +18,6 @@ import * as assert from 'assert';
 import {grpc} from 'google-gax';
 import {google} from '../protos/protos';
 import {Database, Instance, Spanner} from '../src';
-import {MultiplexedSession} from '../src/multiplexed-session';
 import {MutationSet} from '../src/transaction';
 import protobuf = google.spanner.v1;
 import v1 = google.spanner.v1;
@@ -2025,49 +2024,4 @@ describe('End to end tracing headers', () => {
       txn.end();
     }
   });
-
-  it('should schedule background timers in ROOT_CONTEXT to prevent trace/memory leaks', async () => {
-    const originalSetInterval = global.setInterval;
-    let capturedSchedulingContext: any;
-    
-    // Create an active user span context
-    const otelApi = require('@opentelemetry/api');
-    const userTracer = otelApi.trace.getTracer('test-tracer');
-    const userSpan = userTracer.startSpan('User.API.Request');
-    
-    try {
-      await otelApi.context.with(otelApi.trace.setSpan(otelApi.ROOT_CONTEXT, userSpan), async () => {
-        // Monkey-patch setInterval to capture context exactly at scheduling time
-        (global as any).setInterval = function (callback: any, ms: any) {
-          capturedSchedulingContext = otelApi.context.active();
-          // Return a mock handle
-          return {
-            unref: () => {},
-          };
-        };
-
-        // Trigger the MultiplexedSession _maintain() schedule method
-        // (Accessing private method for targeted unit verification)
-        const mux = new MultiplexedSession({} as any);
-        (mux as any)._maintain();
-      });
-
-      // Verify that scheduling DID NOT capture the active user span context,
-      // but instead securely captured ROOT_CONTEXT
-      assert.strictEqual(
-        otelApi.trace.getSpan(capturedSchedulingContext),
-        undefined,
-        'setInterval MUST be scheduled under ROOT_CONTEXT'
-      );
-      assert.strictEqual(
-        capturedSchedulingContext,
-        otelApi.ROOT_CONTEXT,
-        'setInterval context did not match ROOT_CONTEXT'
-      );
-    } finally {
-      // Clean up global state
-      global.setInterval = originalSetInterval;
-      userSpan.end();
-    }
-  });
 });
diff --git a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts
index 1bca56bcb64f..34f9e658c959 100644
--- a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts
+++ b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts
@@ -16,7 +16,7 @@ import * as crypto from 'crypto';
 import * as os from 'os';
 import * as process from 'process';
 import {MeterProvider, MetricReader} from '@opentelemetry/sdk-metrics';
-import {Counter, Histogram} from '@opentelemetry/api';
+import {Counter, Histogram, context, ROOT_CONTEXT} from '@opentelemetry/api';
 import {detectResources, Resource} from '@opentelemetry/resources';
 import {GcpDetectorSync} from '@google-cloud/opentelemetry-resource-util';
 import * as Constants from './constants';
@@ -83,9 +83,11 @@ export class MetricsTracerFactory {
     );
 
     // Start the Tracer cleanup task at an interval
-    this._intervalTracerCleanup = setInterval(
-      this._cleanMetricsTracers.bind(this),
-      Constants.TRACER_CLEANUP_INTERVAL_MS,
+    this._intervalTracerCleanup = context.with(ROOT_CONTEXT, () =>
+      setInterval(
+        this._cleanMetricsTracers.bind(this),
+        Constants.TRACER_CLEANUP_INTERVAL_MS,
+      ),
     );
     // unref the interval to prevent it from blocking app termination
     // in the event loop
