Controlling Resource Access with Semaphores
In this lesson, you will learn how to prevent resource exhaustion by using semaphores to strictly limit the number of concurrent asynchronous operations.
The Problem: When Everything Happens at Once
Imagine your Node.js application needs to interact with a third-party API that rate-limits you to 5 requests at a time. If you trigger 100 requests simultaneously, your application will get blocked, or worse, crash. While Promise.all is great for concurrency, it doesn't provide a "throttle" for shared resources. This is where a Semaphore comes in.
A semaphore acts like a bouncer at a club. It maintains a counter of "permits." When an async task starts, it must "acquire" a permit. If the counter hits zero, the next task must wait in line until another task "releases" its permit.
Implementing a Simple Semaphore
In Node.js, we can build a basic semaphore using a simple queue. Here is a conceptual look at how you control concurrency:
class Semaphore {
constructor(limit) {
this.limit = limit;
this.count = 0;
this.queue = [];
}
async acquire() {
if (this.count < this.limit) {
this.count++;
return;
}
// Wait until a permit is released
return new Promise(resolve => this.queue.push(resolve));
}
release() {
this.count--;
if (this.queue.length > 0) {
this.count++;
const next = this.queue.shift();
next(); // Resume the waiting task
}
}
}
// Usage
const sem = new Semaphore(2); // Only 2 allowed at once
async function processTask(id) {
await sem.acquire();
console.log(`Task ${id} running...`);
await new Promise(r => setTimeout(r, 1000)); // Simulate work
sem.release();
}
Why This Matters
By forcing tasks to wait for an available "slot," you keep your system stable. You aren't just managing the event loop; you are managing the lifecycle of external resource usage. Without this pattern, spikes in traffic can lead to "thundering herd" problems where your application tries to open too many database connections or file descriptors, leading to memory pressure and degraded performance.
Key Takeaway: A semaphore allows you to throttle throughput by gating access to restricted resources, ensuring your application only performs as many concurrent operations as your infrastructure can safely handle.