Skip to content
DevDepth
← Back to all articles

In-Depth Article

How React Priority Works: Lanes, Event Priority, Scheduler, and Starvation

Understand React's priority system through event priority, lane bitmasks, getNextLanes, Scheduler task priorities, and starvation protection.

Published: Updated: 10 min readreact-internals

React's concurrent features only make sense if React can answer one question correctly:

Which update matters most right now?

Typing into an input and rendering a huge background list are both "updates," but they should not be treated the same way. If React gave them identical urgency, the UI would feel sluggish exactly when the user needs it to stay responsive.

That is why React has a priority model.

Many explanations summarize it as "React uses lanes and Scheduler priorities." That is directionally right, but if you want a source-level mental model, it helps to separate the story into three linked layers:

  1. Event priority decides how urgent the update feels at the moment it is created
  2. Lanes represent that work inside the reconciler
  3. Scheduler priority decides when concurrent work should get CPU time

This article walks through how those layers fit together, how getNextLanes chooses work, why higher-priority updates can interrupt lower-priority renders, and how React avoids starving low-priority work forever.

If you want the broader backdrop first, this article pairs well with React concurrent rendering: the underlying principle, React time slicing: how Fiber, Scheduler, yielding, and resumption work, React useState batching: how UpdateQueue, lanes, and scheduling work, and how React updates work from setState to a DOM update.

1. The priority pipeline in one view

At a high level, React's priority flow looks like this:

event happens
-> React assigns an event priority
-> the update gets a lane
-> the lane is merged into root.pendingLanes
-> getNextLanes(...) picks the next work to render
-> React asks Scheduler for a callback if the work is concurrent
-> render runs
-> commit applies the result

That is the flow worth keeping in your head.

Everything else in this article is really just filling in the details of those arrows.

2. Lanes are the reconciler's source of truth

Inside React's reconciler, priority is primarily represented by lanes.

A lane is a bit inside a 31-bit bitmask. Instead of storing one single priority number, React stores sets of pending work as bit patterns.

You do not need to memorize the exact binary values, but the important intuition is:

  • each lane represents one priority channel or lane family
  • multiple lanes can be pending at the same time
  • smaller, more urgent lanes are selected before less urgent ones

At the source level, React defines lane families such as:

  • SyncLane
  • InputContinuousLane
  • DefaultLane
  • TransitionLanes
  • RetryLanes
  • IdleLane
  • OffscreenLane

That is already more expressive than a single "priority = 3" style number.

It lets React say things like:

  • there is urgent input work pending
  • there is also a transition in flight
  • some retry work is suspended
  • some hidden tree work exists offscreen

In other words:

Lanes are not just a severity score. They are React's internal representation of what kinds of work are waiting.

3. Why React uses bitmasks instead of one priority number

This is where the lane model becomes elegant.

With a single number, React can say "this task is high priority" but it becomes awkward to represent multiple pending priorities at once.

With bitmasks, React can do all of these efficiently:

const pending = SyncLane | TransitionLane1 | TransitionLane2;

const merged = laneA | laneB;
const includesTarget = (pending & targetLane) !== NoLanes;
const highestLane = pending & -pending;

That gives React a few important advantages:

  • it can represent multiple pending priorities in one value
  • merging updates is cheap
  • checking whether a priority group is present is cheap
  • selecting the highest-priority pending lane is cheap

That is one of the biggest reasons React moved away from the old expiration-time-only model.

4. Event priority is how updates enter the system

Before an update becomes lane work, React first decides how urgent the triggering event is.

In current React source, the event-priority layer maps like this:

  • DiscreteEventPriority -> SyncLane
  • ContinuousEventPriority -> InputContinuousLane
  • DefaultEventPriority -> DefaultLane
  • IdleEventPriority -> IdleLane

So when people say "clicks are higher priority than background work," the source-level version is:

The event context influences which lane the update is assigned to.

A practical mental model

  • a click or key press usually enters through a discrete event path
  • a continuous interaction like scroll or drag can use continuous priority
  • updates from timers, passive effects, or other non-urgent sources usually land in default priority
  • explicitly deferred work such as startTransition(...) uses transition lanes

That means the first priority decision happens when the update is created, not only later when Scheduler gets involved.

5. pendingLanes is where the root remembers outstanding work

Once React assigns a lane, it merges that lane into the root's pending work.

A simplified mental model looks like this:

const root = {
  pendingLanes: SyncLane | TransitionLane1,
  suspendedLanes: NoLanes,
  pingedLanes: NoLanes,
  expiredLanes: NoLanes,
};

The important field here is pendingLanes.

It means:

  • this root has urgent work waiting
  • it also has deferred transition work waiting

That is the state getNextLanes(...) reads before each render attempt.

6. getNextLanes decides what React should work on next

If you want to understand interruption, this is one of the most important functions in the reconciler.

At a simplified level, getNextLanes(root, wipLanes) does this:

  1. Look at the root's pending lanes
  2. Exclude lanes that are suspended unless they were pinged
  3. Prefer the highest-priority available lanes
  4. Compare them with the work already in progress
  5. Decide whether React should continue the current render or switch to something more urgent

So the core intuition from many blog posts is still useful:

React picks the most urgent renderable lane first.

But the real function is more nuanced than just pendingLanes & -pendingLanes.

It also considers:

  • suspended lanes
  • pinged lanes
  • expired lanes
  • entangled lanes
  • the current work-in-progress lanes

That nuance matters because React is not only asking "what is highest priority?" It is also asking:

What is the highest-priority work that is actually runnable right now, and should it replace what I am already rendering?

7. Why higher-priority work can interrupt lower-priority rendering

Suppose React is rendering a large transition update.

While that render is in progress, the user clicks a button or types into an input. That new update lands in a more urgent lane.

Now React has a conflict:

  • old work is already in progress
  • new work is more urgent

If getNextLanes(...) chooses the new urgent lane instead of the old work-in-progress lane, React can abandon the current draft tree and prepare a fresh stack for the higher-priority work.

That is the source-level meaning of "higher-priority work cuts in line."

It is not that React freezes a JavaScript thread and swaps tasks like an operating system. It is that:

  • Fiber makes render work resumable
  • lanes tell React which work is more urgent
  • getNextLanes(...) lets React decide whether to continue or restart

That is why time slicing and the lane model belong to the same bigger story.

This is where many explanations get blurry.

Lanes are the reconciler's priority model. Scheduler priorities are the task-runner's priority model.

Scheduler exposes five priority levels:

  • ImmediatePriority
  • UserBlockingPriority
  • NormalPriority
  • LowPriority
  • IdlePriority

These are task priorities, not lane families.

So React needs a translation layer when it schedules concurrent work.

At the Scheduler layer, the default timeout buckets are also part of the story:

  • ImmediatePriority: -1
  • UserBlockingPriority: about 250ms
  • NormalPriority: about 5000ms
  • LowPriority: about 10000ms
  • IdlePriority: effectively no expiration timeout

Those numbers belong to Scheduler task timing, not to the lane model itself.

The important nuance in current React

If you learned React priority from older explanations, you may have heard something like:

SyncLane maps to ImmediatePriority.

That shorthand is not the safest way to explain current source.

In today's root scheduler logic, React first converts lanes to an event priority, then maps that to a Scheduler priority for concurrent callbacks:

  • discrete or continuous event priority -> UserBlockingPriority
  • default event priority -> NormalPriority
  • idle event priority -> IdlePriority

And the source explicitly notes that although Scheduler still has ImmediatePriority, React no longer uses it as the normal path for sync root work because sync work is flushed through microtasks.

That distinction is worth remembering:

The reconciler decides what work exists with lanes. Scheduler decides when concurrent callbacks run. They are connected, but they are not interchangeable.

9. Not every update goes through the same scheduling path

Another useful correction is this:

React priority is not only "lane -> Scheduler callback."

Some work is flushed synchronously or via microtasks. Some work is scheduled as concurrent callbacks. Some work is restarted because a higher-priority lane arrived.

So a better mental model is:

  • lanes are always the reconciler's bookkeeping model
  • Scheduler participates when React needs time-sliced callback scheduling
  • sync work has its own more immediate flushing path

This is why reading only the Scheduler constants does not fully explain React priority behavior.

10. How React prevents starvation

A natural question comes up quickly:

If urgent work keeps arriving, can low-priority work be delayed forever?

React guards against that with expiration tracking.

This is another place where the old and new mental models meet:

Lanes replaced expiration time as the primary way React represents priority, but expiration still exists as a starvation safeguard.

Each pending lane can get an expiration time. When React notices that a lane has been waiting too long, it marks that lane as expired on the root.

The important idea is not the exact timestamp math. The important idea is:

Once a lane is considered starved, React stops treating it like optional deferred work and forces it toward completion.

This is React's safeguard against the "the user keeps clicking forever, so the background transition never finishes" problem.

A subtle but important refinement

It is common to hear this explained as "the lane becomes SyncLane."

That is a useful shortcut for intuition, but the source-level wording is:

  • the lane is marked in root.expiredLanes
  • expired work is treated as urgent and flushed accordingly

So the behavioral takeaway is right even if the literal implementation is more precise than "rewrite the bit to SyncLane."

11. A complete example from update creation to interruption

Here is a small component that shows the shape of the problem:

import { useState, useTransition } from "react";

export default function SearchBox() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const next = e.target.value;

    setQuery(next);

    startTransition(() => {
      setResults(expensiveSearch(next));
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <p>Updating results...</p>}
      <ResultsList items={results} />
    </div>
  );
}

The key idea is:

  • updating query is urgent because the input should stay responsive
  • updating results can be deferred because it may involve heavier rendering

From React's priority point of view, that often becomes a lane split between more urgent input work and less urgent transition work.

Imagine this sequence:

  1. You call startTransition(() => setResults(...))
  2. React assigns that update to a transition lane
  3. The lane is merged into root.pendingLanes
  4. getNextLanes(...) selects the transition work
  5. React starts a concurrent render
  6. Before the render finishes, the user types into an input
  7. The new input update lands in a more urgent lane
  8. React re-runs lane selection and switches to the urgent work first
  9. The old transition render is resumed later or restarted from fresher state

That entire story is the real meaning of "React keeps urgent interactions responsive."

It is not one feature. It is the cooperation of:

  • event priority
  • lane assignment
  • root lane bookkeeping
  • next-lane selection
  • scheduler callbacks
  • interruptible rendering

12. Common misconceptions that make this topic feel harder than it is

"React has only one priority system"

Not really. At minimum, you should distinguish event priority, lanes, and Scheduler task priority.

"Scheduler is the real source of truth"

No. Scheduler controls when a callback runs. The reconciler's lane model is where React tracks and chooses actual update work.

"getNextLanes just picks the rightmost 1 bit"

That is a useful starting intuition, but the real function also considers suspension, pinging, expiration, entanglement, and in-progress work.

"Expired work means React literally rewrites the lane into SyncLane"

The more precise implementation is that React marks the lane as expired and forces urgent completion behavior from there.

13. The interview answer version

If someone asks, "How does React priority work?" a solid answer is:

  1. React first derives update urgency from the event context
  2. It assigns the update to one or more lanes, which are bitmask-based priority channels inside the reconciler
  3. The root merges those lanes into pendingLanes
  4. Before each render, getNextLanes(...) chooses the highest-priority runnable work
  5. If more urgent work appears, React can interrupt lower-priority concurrent rendering and restart from the more urgent lane
  6. When React needs concurrent scheduling, it maps lane-derived event priority to Scheduler task priority
  7. If low-priority work waits too long, React marks it expired so it will not starve forever

That answer is much stronger than saying only "React uses lanes now instead of expiration times."

14. Final takeaway

React priority is easiest to understand if you stop trying to collapse it into one number.

The real model is:

  • event priority decides how an update enters
  • lanes track and group pending work inside the reconciler
  • getNextLanes(...) decides what should run next
  • Scheduler helps React decide when concurrent callbacks should run
  • expiration prevents low-priority work from waiting forever

Once that clicks, a lot of React internals stop feeling like disconnected tricks. Interruption, time slicing, transitions, and starvation prevention all become parts of the same larger priority pipeline.

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-17

Contact the editor