Starvation in Javascript: When your program remains Hungry
If you've spent time working with JavaScript's asynchronous model, you know that its single-threaded nature is both a blessing and a curse. On one hand, you don't have to worry about complex locking mechanisms. On the other, you might stumble into weird issues where your code just... never runs. One such issue is starvation. What's Starvation? In the context of JavaScript, starvation refers to a situation where a task is perpetually delayed because the event loop is constantly busy with other tasks—usually of higher priority or those scheduled more frequently. Since JavaScript executes code in a single-threaded environment using an event loop, starvation can occur when: Long-running synchronous code blocks the event loop. Microtasks (Promises, MutationObservers, etc.) continuously prevent macrotasks (like setTimeout, setInterval) from executing. Eg : Macrotask Starvation function scheduleMacrotask() { setTimeout(() => { console.log('Macrotask executed'); }, 0); } function floodMicrotasks() { for (let i = 0; i < 1e5; i++) { Promise.resolve().then(() => { // Simulate quick microtasks }); } } scheduleMacrotask(); floodMicrotasks(); You might expect to see "Macrotask executed" logged right away. But the setTimeout callback (a macrotask) gets pushed back behind a flood of microtasks. Microtasks in JavaScript are executed immediately after the currently executing script and before any macrotask. So, if you continuously add microtasks, the macrotask keeps getting pushed back. That's starvation. Why Does Starvation Happen? Starvation usually happens due to: Excessive microtask generation Frequent Promise.resolve().then() in tight loops. Recursive promise chains. Long synchronous blocks Functions doing CPU-heavy work without yielding back to the event loop. Unbalanced priority When you schedule many tasks at one level (e.g., microtasks) but never give time for other levels (e.g., macrotasks). Solution and Workarounds Yield with setTimeoutBreak long synchronous code into smaller chunks using setTimeout to allow the event loop to breathe: function longTask() { for (let i = 0; i < 1e6; i++) { if (i % 10000 === 0) { // Yield to the event loop setTimeout(() => longTask(i + 1), 0); return; } // Do work } } Use await with care If using async/await, be cautious of chaining too many await Promise.resolve() calls without breaks. Consider requestIdleCallback (for non-critical work) requestIdleCallback(() => { // Low-priority background task }); Queue microtasks responsibly Avoid flooding the microtask queue. Instead of a huge batch, spread tasks out: function chunkMicrotasks(data) { function processChunk(index) { if (index >= data.length) return; // Process a small piece for (let i = index; i < index + 100 && i < data.length; i++) { // Do lightweight work } // Schedule next chunk setTimeout(() => processChunk(index + 100), 0); } processChunk(0); } Starvation in JavaScript isn't just a theoretical concern—it can cause real performance and responsiveness issues in your applications. Understanding how the event loop, microtasks, and macrotasks work together gives you the power to avoid these pitfalls. Next time your setTimeout doesn’t seem to fire or your app feels sluggish, take a closer look at your microtask usage and the structure of your event loop. Happy

If you've spent time working with JavaScript's asynchronous model, you know that its single-threaded nature is both a blessing and a curse. On one hand, you don't have to worry about complex locking mechanisms. On the other, you might stumble into weird issues where your code just... never runs. One such issue is starvation.
What's Starvation?
In the context of JavaScript, starvation refers to a situation where a task is perpetually delayed because the event loop is constantly busy with other tasks—usually of higher priority or those scheduled more frequently.
Since JavaScript executes code in a single-threaded environment using an event loop, starvation can occur when:
- Long-running synchronous code blocks the event loop.
- Microtasks (Promises, MutationObservers, etc.) continuously prevent macrotasks (like setTimeout, setInterval) from executing.
Eg : Macrotask Starvation
function scheduleMacrotask() {
setTimeout(() => {
console.log('Macrotask executed');
}, 0);
}
function floodMicrotasks() {
for (let i = 0; i < 1e5; i++) {
Promise.resolve().then(() => {
// Simulate quick microtasks
});
}
}
scheduleMacrotask();
floodMicrotasks();
You might expect to see "Macrotask executed" logged right away. But the setTimeout callback (a macrotask) gets pushed back behind a flood of microtasks.
Microtasks in JavaScript are executed immediately after the currently executing script and before any macrotask. So, if you continuously add microtasks, the macrotask keeps getting pushed back. That's starvation.
Why Does Starvation Happen?
Starvation usually happens due to:
-
Excessive microtask generation
- Frequent Promise.resolve().then() in tight loops.
- Recursive promise chains.
-
Long synchronous blocks
- Functions doing CPU-heavy work without yielding back to the event loop.
-
Unbalanced priority
- When you schedule many tasks at one level (e.g., microtasks) but never give time for other levels (e.g., macrotasks).
Solution and Workarounds
- Yield with setTimeoutBreak long synchronous code into smaller chunks using setTimeout to allow the event loop to breathe:
function longTask() {
for (let i = 0; i < 1e6; i++) {
if (i % 10000 === 0) {
// Yield to the event loop
setTimeout(() => longTask(i + 1), 0);
return;
}
// Do work
}
}
Use await with care
If using async/await, be cautious of chaining too many await Promise.resolve() calls without breaks.Consider requestIdleCallback (for non-critical work)
requestIdleCallback(() => {
// Low-priority background task
});
- Queue microtasks responsibly Avoid flooding the microtask queue. Instead of a huge batch, spread tasks out:
function chunkMicrotasks(data) {
function processChunk(index) {
if (index >= data.length) return;
// Process a small piece
for (let i = index; i < index + 100 && i < data.length; i++) {
// Do lightweight work
}
// Schedule next chunk
setTimeout(() => processChunk(index + 100), 0);
}
processChunk(0);
}
Starvation in JavaScript isn't just a theoretical concern—it can cause real performance and responsiveness issues in your applications.
Understanding how the event loop, microtasks, and macrotasks work together gives you the power to avoid these pitfalls.
Next time your setTimeout doesn’t seem to fire or your app feels sluggish, take a closer look at your microtask usage and the structure of your event loop.
Happy