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);

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.