Event Loop Starvation

Event loop starvation occurs when continuously scheduled JavaScript work prevents other queued work, such as timers, input handling, or rendering opportunities, from getting a chance to run. In browsers, this can make the UI appear frozen even when the program is still executing.

JavaScript on the browser's main thread processes work through an event loop. Synchronous JavaScript runs to completion before the browser can move on to other work. If code repeatedly adds more work and never allows control to return to the event loop, pending tasks can be delayed indefinitely or for a very long time.

A common case involves microtasks such as promise callbacks. After a task finishes, the browser processes pending microtasks before continuing to later event-loop work. If each microtask schedules another microtask, the microtask queue can keep growing or remain continuously non-empty, delaying timers, user input, and rendering opportunities.

Starvation can also happen with tasks when a callback repeatedly schedules more callbacks. The important distinction is not merely that there is a large queue; starvation means some category of work is prevented from receiving execution time because another category keeps taking it.

The usual fix is to yield periodically. For example, split CPU-heavy work into smaller chunks and schedule later chunks through a mechanism that lets the browser regain control between them. For UI-heavy code, yielding gives the browser opportunities to process input and render updates.

Key points

Interview questions

What is event loop starvation in a browser?

Event loop starvation happens when one kind of JavaScript work keeps the event loop busy so other pending work cannot run promptly. A common example is repeatedly scheduling microtasks. Because JavaScript execution is run-to-completion and microtasks are processed before later task work, an unbounded microtask chain can delay timers, input handling, and rendering opportunities.

Why can recursive `queueMicrotask()` calls prevent a timer from running?

A timer callback is a task, while `queueMicrotask()` adds work to the microtask queue. After the current task finishes, the browser processes pending microtasks before moving on to another task. If every microtask schedules another microtask, the queue may never become empty, so the timer can remain pending indefinitely.

How would you fix a CPU-heavy operation that is freezing the UI?

I would first divide the computation into smaller chunks rather than performing it as one long synchronous operation. Between chunks, I would yield so the browser can process input and rendering opportunities. The exact scheduling mechanism depends on the workload and browser requirements, but the key goal is to avoid monopolizing the main thread.

Is every large microtask queue an example of event loop starvation?

No. A large queue can simply represent a burst of work that eventually completes and allows other work to proceed. Starvation is more specifically about some work being prevented from getting execution time because another stream keeps consuming scheduling opportunities. The practical concern is therefore both the amount of queued work and whether that work is continuously replenished.

What is the relationship between event loop starvation and rendering?

Rendering requires the browser to regain control of the main thread at suitable points. If JavaScript continuously occupies the main thread, especially through long tasks or an endlessly replenished microtask queue, the browser may be unable to reach a rendering opportunity promptly. The result can be delayed visual updates and poor responsiveness.