ConcurrencyTypeScriptverifiedVerified
Semaphore Pattern in TypeScript
Control access to a finite pool of resources by maintaining a counter that threads atomically increment (release) and decrement (acquire), blocking when the count reaches zero.
How to Implement the Semaphore Pattern in TypeScript
1Step 1: Implement the Semaphore with permit management
class Semaphore {
private permits: number;
private waiters: Array<() => void> = [];
constructor(initialPermits: number) {
if (initialPermits < 1) throw new Error("Permits must be >= 1");
this.permits = initialPermits;
}
acquire(): Promise<void> {
if (this.permits > 0) {
this.permits--;
return Promise.resolve();
}
return new Promise<void>(resolve => this.waiters.push(resolve));
}
release(): void {
const next = this.waiters.shift();
if (next) {
next(); // pass permit directly to next waiter
} else {
this.permits++;
}
}
async withPermit<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
get available(): number { return this.permits; }
get queued(): number { return this.waiters.length; }
}2Step 2: Limit concurrent tasks using the semaphore
const semaphore = new Semaphore(3);
const tasks = Array.from({ length: 10 }, (_, i) =>
semaphore.withPermit(async () => {
await new Promise(r => setTimeout(r, 100));
return `task-${i}`;
})
);
const results = await Promise.all(tasks);
console.log(results);// ── Rate Limiter Using Semaphore for Concurrent API Calls ─────────
class Semaphore {
private permits: number;
private waiters: Array<{ resolve: () => void; reject: (e: Error) => void }> = [];
constructor(private maxConcurrency: number) {
this.permits = maxConcurrency;
}
acquire(signal?: AbortSignal): Promise<void> {
if (this.permits > 0) {
this.permits--;
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const waiter = { resolve, reject };
this.waiters.push(waiter);
signal?.addEventListener("abort", () => {
const idx = this.waiters.indexOf(waiter);
if (idx !== -1) {
this.waiters.splice(idx, 1);
reject(new Error("Semaphore acquisition aborted"));
}
}, { once: true });
});
}
release(): void {
const next = this.waiters.shift();
if (next) {
next.resolve();
} else {
this.permits++;
}
}
async withPermit<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
await this.acquire(signal);
try {
return await fn();
} finally {
this.release();
}
}
}
interface RateLimiterOptions {
maxConcurrent: number;
minIntervalMs?: number; // token-bucket style spacing
}
interface ApiResponse<T> {
data: T;
requestId: string;
durationMs: number;
}
class ApiRateLimiter {
private semaphore: Semaphore;
private lastCallTime = 0;
constructor(private options: RateLimiterOptions) {
this.semaphore = new Semaphore(options.maxConcurrent);
}
async call<T>(
fn: () => Promise<T>,
requestId: string,
signal?: AbortSignal
): Promise<ApiResponse<T>> {
return this.semaphore.withPermit(async () => {
// Enforce minimum spacing between requests
if (this.options.minIntervalMs) {
const elapsed = Date.now() - this.lastCallTime;
if (elapsed < this.options.minIntervalMs) {
await new Promise(r =>
setTimeout(r, this.options.minIntervalMs! - elapsed)
);
}
}
const start = Date.now();
this.lastCallTime = Date.now();
const data = await fn();
return {
data,
requestId,
durationMs: Date.now() - start,
};
}, signal);
}
get available(): number { return (this.semaphore as unknown as { permits: number }).permits; }
}
// Usage: limit to 5 concurrent API calls with 100ms minimum spacing
const limiter = new ApiRateLimiter({
maxConcurrent: 5,
minIntervalMs: 100,
});
async function fetchUser(id: string): Promise<{ id: string; name: string }> {
// Simulated API call
return { id, name: `User ${id}` };
}
const userIds = Array.from({ length: 20 }, (_, i) => String(i + 1));
const responses = await Promise.all(
userIds.map(id =>
limiter.call(() => fetchUser(id), `req-${id}`)
)
);
export { ApiRateLimiter, Semaphore, type RateLimiterOptions, type ApiResponse };Semaphore Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Semaphore Pattern in the Real World
“Imagine a car park with exactly three spaces. A ticket machine at the entrance (the semaphore) issues a ticket only if spaces remain, lifting the barrier; arriving drivers with no ticket available must wait. When a car exits, the machine automatically increments its counter and releases the next waiting driver — the car park never exceeds capacity.”