Last active
May 9, 2026 13:03
-
-
Save adeisbright/3a3471835e7234870412e3aab8ce29ec to your computer and use it in GitHub Desktop.
An example on how to handle thunder herd problem
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Redis from "ioredis"; | |
| interface TokenResponse { | |
| token: string; | |
| expires_in: string; | |
| } | |
| interface AuthClientConfig { | |
| url: string; | |
| authPayload: Record<string, unknown>; | |
| cacheKey?: string; | |
| retryAttempts?: number; | |
| retryDelayMs?: number; | |
| } | |
| export class AuthTokenService { | |
| private inFlightPromise: Promise<string> | null = null; | |
| constructor( | |
| private readonly redis: Redis, | |
| private readonly config: AuthClientConfig | |
| ) {} | |
| async getToken(): Promise<string> { | |
| const { | |
| cacheKey | |
| } = this.config; | |
| // 1. Local memory lock | |
| if (this.inFlightPromise) { | |
| return this.inFlightPromise; | |
| } | |
| // 2. Shared cache lookup | |
| const cachedToken = await this.redis.get(cacheKey); | |
| if (cachedToken) { | |
| return cachedToken; | |
| } | |
| // 3. Acquire token | |
| this.inFlightPromise = this.acquireToken(); | |
| try { | |
| return await this.inFlightPromise; | |
| } finally { | |
| this.inFlightPromise = null; | |
| } | |
| } | |
| private async acquireToken(): Promise<string> { | |
| const { | |
| url, | |
| authPayload, | |
| retryAttempts = 3, | |
| retryDelayMs = 500, | |
| cacheKey | |
| } = this.config; | |
| for (let attempt = 0; attempt <= retryAttempts; attempt++) { | |
| try { | |
| const response = await fetch(url, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify(authPayload), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Auth server failed: ${response.status}`); | |
| } | |
| const data = (await response.json()) as TokenResponse; | |
| const expirationSeconds = Math.max( | |
| 60, | |
| Math.floor( | |
| (new Date(data.expires_in).getTime() - Date.now()) / 1000 | |
| ) - 30 | |
| ); | |
| await this.redis.setex( | |
| cacheKey, | |
| expirationSeconds, | |
| data.token | |
| ); | |
| return data.token; | |
| } catch (error) { | |
| if (attempt === retryAttempts) { | |
| throw error; | |
| } | |
| await this.sleep( | |
| retryDelayMs * Math.pow(2, attempt) | |
| ); | |
| } | |
| } | |
| throw new Error("Failed to acquire token"); | |
| } | |
| private sleep(ms: number): Promise<void> { | |
| return new Promise(resolve => { | |
| setTimeout(resolve, ms); | |
| }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment