Understanding Asynchronous Execution with a Queue in JavaScript

I'm Shubham (@shubhamsinghbundela), I'm a Software Engineer, a Full-stack developer, a tech enthusiast, and a technical writer here on @Hashnode. I have a strong zeal to share my acquired knowledge and I am also willing to learn from others.
When we say JavaScript is asynchronous, what does that really mean?
It means:
A task can start now and finish later, without blocking other code.
But when we have multiple async tasks, we need control.
What if:
We have 100 API calls?
Or 100 file uploads?
Or 100 video processing jobs?
If we run all of them at once, we may overload the system.
So we use a queue with concurrency control.
The Goal
We want to:
Execute asynchronous tasks.
Allow only N tasks to run at the same time.
Queue extra tasks.
Start queued tasks automatically when one finishes.
Call a callback when each task completes.
Here is the implementation:
class CallbackPool {
constructor(limit) {
this.limit = limit;
this.queue = [];
this.active = 0;
}
run(task, onComplete) {
this.queue.push({ task, onComplete });
this._next();
}
_next() {
while (this.active < this.limit && this.queue.length > 0) {
const { task, onComplete } = this.queue.shift();
this.active++;
task((err, data) => {
this.active--;
if (onComplete) {
onComplete(err, data);
}
this._next();
});
}
}
}
The Asynchronous Task
Here’s our async task:
const task = (cb) => {
setTimeout(() => {
cb(null, "done");
}, 20);
};
This is asynchronous because:
setTimeoutschedules work.It does NOT block.
It returns immediately.
The callback runs later (after 20ms).
What Happens When We Run 5 Tasks?
const pool = new CallbackPool(2);
for (let i = 0; i < 5; i++) {
pool.run(task, () => console.log("Task Done"));
}
Concurrency limit = 2
Let’s simulate step by step.
Step 1: First Task
Queue:
[T1]
_next() runs.
Condition:
active < limit → 0 < 2 ✅
T1 starts.
active = 1
T1 schedules setTimeout and exits immediately.
Step 2: Second Task
Queue:
[T2]
Condition:
1 < 2 ✅
T2 starts.
active = 2
Now 2 tasks are running.
Step 3: Third Task
Queue:
[T3]
Condition:
2 < 2 ❌
T3 stays in queue.
Important Understanding
At this moment:
T1 and T2 are waiting for their 20ms timer.
JavaScript is NOT blocked.
The program continues.
This is asynchronous execution.
After 20ms (Event Loop in Action)
T1 finishes.
Its callback runs:
this.active--;
this._next();
Now:
active = 1
Since active < limit:
T3 starts.
active = 2
Queue: [T4, T5]
The Pattern
Every time a task finishes:
active decreases.
Completion callback runs.
_next()checks if another task can start.Next queued task begins.
So execution becomes:
Start 2 →
One finishes →
Start next →
Repeat
Why This Explains Asynchronous Behavior Clearly
This example shows:
Tasks do not complete immediately.
The system reacts when callbacks fire.
The queue stores waiting tasks.
The active counter controls concurrency.
Scheduling happens dynamically.
This is not parallel threads.
This is event-loop-driven asynchronous scheduling.
More Visualization below:
Time →
--------------------------------------
T1: |------20ms------|
T2: |------20ms------|
T3: |------20ms------|
T4: |------20ms------|
T5: |------20ms------|
Maximum running at once = 2
Core Concept
Asynchronous + Queue works like this:
Add task → goes into queue.
If slot free → start immediately.
If not → wait.
When task finishes → free slot.
Start next task automatically.
The key is:
Completion triggers the next execution.




