Skip to content

Commit 809dfed

Browse files
committed
fix: singleton SecretManagerServiceClient to prevent gRPC channel leak + OOM
Every call to getSecret() was creating a new SecretManagerServiceClient, which opens a new gRPC channel. These channels were not being closed or garbage collected, causing memory to grow ~488 MiB OOM within ~28 minutes on a 512M Cloud Run instance. Fix: use a module-level singleton client (created once per container instance) and cache secret values in memory (secrets don't change at runtime). This eliminates both the gRPC channel leak and the Secret Manager round-trips on every webhook request.
1 parent c41d1eb commit 809dfed

File tree

1 file changed

+19
-2
lines changed

1 file changed

+19
-2
lines changed

src/utils/get-secret.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@ import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
22
import config from "../config.js";
33

44
/**
5-
* Load a secret from Secret Manager
5+
* Singleton gRPC client — creating a new SecretManagerServiceClient per request
6+
* leaks gRPC channels and causes memory growth (~488 MiB OOM within 30 min).
7+
*/
8+
const secretManager = new SecretManagerServiceClient();
9+
10+
/**
11+
* In-memory cache for secrets. Secrets don't change during an instance's lifetime,
12+
* so we can safely cache them and avoid repeated Secret Manager round-trips.
13+
*/
14+
const secretCache = new Map<string, string>();
15+
16+
/**
17+
* Load a secret from Secret Manager (cached per instance lifetime)
618
*/
719
export default async function getSecret(secretId: string): Promise<string> {
20+
const cached = secretCache.get(secretId);
21+
if (cached) {
22+
return cached;
23+
}
24+
825
try {
9-
const secretManager = new SecretManagerServiceClient();
1026
const secretFullResourceName = `projects/${config.GCP_PROJECT_ID}/secrets/${secretId}/versions/latest`;
1127
const [version] = await secretManager.accessSecretVersion({
1228
name: secretFullResourceName,
@@ -20,6 +36,7 @@ export default async function getSecret(secretId: string): Promise<string> {
2036
);
2137
}
2238

39+
secretCache.set(secretId, secret);
2340
return secret;
2441
} catch (error) {
2542
console.error(

0 commit comments

Comments
 (0)