A Practical Overview of Scheduler Implementations in Solana Validators

A Practical Overview of Scheduler Implementations in Solana Validators

Contributors: Veronika Bauer, Filip Rezabek, Prof. Georg Carle


In blockchains like Solana, the leader node is responsible for creating blocks. Each block consists of transactions that must be executed by the network. Since high-throughput blockchains often receive more transactions than can be included immediately, the leader needs a policy for deciding which transactions to process next.

On Solana, this responsibility sits with the scheduler. The scheduler determines the order in which incoming transactions are assigned for execution. This affects both user experience and validator economics: users can submit priority transactions [7] by paying an additional fee, and validators can earn more when high-value transactions are selected efficiently. Solana charges a base fee for every submitted transaction, while users may optionally add a priority fee [8] to improve confirmation speed. The size of that priority fee depends on network demand and can vary substantially during periods of congestion, such as NFT mints, liquidations, or arbitrage opportunities.

This creates a scheduling trade-off. A validator may prefer high-priority transactions because they pay more, but transactions that consume fewer compute units can be cheaper and easier to execute. As a result, scheduling is not just a performance problem; it is also a validator revenue and resource allocation problem. This post provides an overview of different scheduler implementations in Solana validators, focusing on Agave and Firedancer / Frankendancer.


What is a scheduler?

A scheduler is part of Solana’s Transaction Processing Unit, or TPU. The TPU receives transactions, verifies them, forwards them when appropriate, and eventually executes them during the banking stage. A useful overview of the TPU is available in the Anza validator documentation [1], and Andrew Fitzgerald has a detailed explanation of the Solana banking stage and scheduler [2].

At a high level, transaction processing works as follows:

  1. A transaction is sent to a validator.
  2. The transaction is received by the QUIC streamer.
  3. During the SigVerify stage, transactions are deduplicated and signatures are verified.
  4. Invalid transactions are dropped.
  5. If the node is not the current leader, the forwarding stage forwards the transaction to the leader.
  6. If the node is the current leader, the transaction enters the banking stage.
  7. The banking stage schedules and executes transactions while Proof of History is produced.
  8. Once a batch has been built, it is broadcast through Turbine so downstream validators can receive and vote on the block.

The faster a transaction is scheduled and processed by the banking stage, the sooner it can be propagated and finalized. Therefore, the scheduler plays a central role in both transaction latency and block construction quality.


Figure 1: Solana transaction processing unit (TPU). Adapted from the Anza TPU documentation [1] and Andrew Fitzgerald’s banking stage and scheduler overview [2].


Scheduler implementations in Agave

Agave has gone through several scheduler designs. Before Agave v1.18, it used a thread-local multi-iterator rather than a centralized scheduler. Worker threads pulled transactions directly from a shared channel. Each thread maintained its own transaction queue, sorted transactions by priority, selected a transaction, and attempted to acquire the required locks.


This design could create severe delays under high load. During periods such as NFT mints, many high-priority transactions may conflict on the same accounts. In that case, worker threads repeatedly fail to acquire locks, and different threads may compete for the same locked accounts. To address this, Agave introduced a central scheduler in v1.18, described in Anza’s post on the central scheduler in Agave v1.18 [3] and summarized in Helius’s Solana v1.18 update [4].

Agave currently provides two scheduler implementations:

  • The central scheduler, which has been the standard scheduler since Agave v2.0, according to the Helius Agave v2.0 update [5].
  • The greedy scheduler, introduced in Agave v2.1 but opt-in rather than enabled by default, as described in the Helius Agave v2.1 update [6].

Agave central scheduler

The central scheduler was introduced to prioritize high-value transactions while reducing account lock conflicts. Its goal is to schedule high-priority transactions that do not conflict with each other, then send them to worker threads for execution.

Incoming transactions are inserted into a priority queue. Agave’s priority calculation is based on fees and requested compute units:

Priority = ((priority fee Ă— requested compute units) + base fee) / (1 + requested compute units)

This formula means that transactions with higher priority fees can receive higher priority, but the requested compute units also matter. A transaction that requests fewer compute units can be comparatively more attractive, all else equal.

The central scheduler then pops high-priority transactions from the queue and uses them to build a **priority graph**. This graph models dependencies between transactions. Transactions that conflict with each other are connected by edges, while non-conflicting transactions can be scheduled in parallel. The graph is a directed acyclic graph used to decide which transactions can be safely assigned to worker threads.

The central scheduler works roughly as follows:
  1. Determine how many compute units can still be assigned to each worker thread.
  2. Select threads whose assigned compute units are below the configured limit.
  3. Pick the next transaction from the priority graph.
  4. Acquire the required locks.
  5. Assign the transaction to a worker thread.
  6. Update the worker’s compute unit count.
  7. Remove the scheduled transaction from the graph.
  8. Insert another transaction from the priority queue.
  9. Repeat while worker threads still have available compute capacity.

A key benefit of the central scheduler is its centralized view of account locks. If a selected transaction requires locks already held by a specific thread, the scheduler attempts to place it on that thread. If no thread currently holds the required locks, the transaction may be scheduled to any available thread.


The scheduler is also conservative around conflicts. If a high-priority transaction cannot be scheduled immediately because of conflicts, it remains in the priority graph. The scheduler avoids scheduling lower-priority transactions that would conflict with the held-back transaction.

The trade-off is overhead. Constructing and maintaining the priority graph can become expensive under high load. As noted in discussions of the central scheduler [3], the graph improves lock-aware scheduling, but it also introduces work that can slow down scheduling when transaction volume is high.


Agave greedy scheduler

The greedy scheduler was introduced to reduce scheduling overhead and improve throughput potential. Unlike the central scheduler, it does not build a dependency graph first. Instead, it tries to schedule transactions immediately in priority order.

The greedy scheduler begins by sanitizing incoming transactions and calculating their priorities. Transactions are inserted into a priority queue ordered by priority. Worker threads are assigned compute-unit budgets, representing how much additional work each thread can accept before reaching its compute limit.

The scheduler then proceeds greedily:

  1. Add all worker threads with the remaining compute-unit budget to the list of schedulable threads.
  2. Select the highest-priority transaction from the priority queue.
  3. Check which locks the transaction requires.
  4. Determine whether the transaction can be assigned to an available thread.
  5. If the transaction conflicts with any transaction in the current batch, complete and send the current batch.
  6. If the transaction still cannot be scheduled, place it in an unscheduled list.
  7. Otherwise, acquire the required locks, assign the transaction to a thread, and update that thread’s budget.
  8. Repeat until no available thread has remaining budget.
  9. Return unscheduled transactions to the priority queue.

The greedy scheduler’s main goal is to schedule the highest-priority transaction as soon as possible. This can reduce latency for high-priority transactions. However, it can also lead to smaller batches.

For example, suppose transactions A, E, and I all have high priority but conflict with each other. The central scheduler may fill a batch with non-conflicting transactions such as A, B, C, and D. The greedy scheduler may instead immediately send a batch containing only A if the next high-priority transaction conflicts with the current batch. This allows the next high-priority transaction to be considered sooner, but at the cost of smaller batch sizes and potentially lower batching efficiency.


The trade-off is therefore clear: the greedy scheduler can improve responsiveness for high-priority transactions, but it may reduce batch size and overall efficiency in some workloads. Helius’s Agave v2.1 update [6] gives a useful overview of why this design was introduced and why it is not enabled by default.


Firedancer / Frankendancer scheduler

Firedancer is a Solana validator implementation developed by Jump Crypto. Frankendancer refers to the transitional architecture that combines Firedancer components with parts of Agave while the Firedancer team works toward removing Agave code. The scheduler behavior in Firedancer can be configured using different modes in the Firedancer codebase [12].

Firedancer supports two main scheduling modes:

Mode Goal Behavior
Balanced Optimize for revenue while keeping block production practical Enabled by default. Votes are always scheduled. Bundles are restricted to one bank tile, while other transactions are paced.
Perf Fill blocks as quickly as possible Votes, transactions, and bundles can be scheduled as needed. The emphasis is on full blocks and throughput.

Older versions also included a bundle mode. This mode enabled the revenue scheduler, which could increase revenue but often resulted in blocks that were not fully occupied. In this mode, votes and bundles were allowed on every tile, while single transactions were scheduled only during the final 50 ms of the slot. This mode has since been deprecated.


Firedancer distinguishes between two important transaction groupings:

  • Microblocks, which contain multiple transactions that can be executed in arbitrary order.
  • Bundles, which contain multiple atomic transactions that must be executed in order.

Bundles are often treated as especially lucrative, so they are prioritized. However, they also carry an important risk: if one transaction in a bundle expires, the entire bundle becomes invalid. Harsh Patel’s article on Firedancer’s transaction scheduler [10] provides a detailed overview of this design.


Firedancer priority queues and treaps

After a transaction is received by Firedancer, it passes a series of checks, including deduplication and blacklist checks. If it passes, its priority is calculated, and it is assigned to a priority queue.

Firedancer uses a treap, which is a balanced binary search tree, for priority ordering. It maintains several treap types:

  • Pending treap
  • Pending votes treap
  • Penalty treap
  • Pending bundles treap

A transaction’s priority is based on its reward relative to compute resources:

priority = reward / compute resources

This means that Firedancer favors transactions that provide a higher reward per unit of compute. However, this is not the whole story. Transactions that allocate large amounts of data are penalized by reducing their reward:

divisor = (allocated data Ă— maximum allowed costs per block) /
          (estimated costs Ă— maximum allowed allocated data per block)

new reward = reward / divisor

As a result, transactions with significant data allocation receive lower priority.

A similar penalty applies to transactions that write to hot accounts. Hot accounts are accounts that have been touched by many transactions, which means other transactions may need to wait before they can access them. These transactions are placed into the penalty treap.

When a transaction can be executed, Firedancer selects the highest-priority transaction from the relevant treap.


Firedancer dropping behavior

If a treap is full, Firedancer may delete transactions. The transaction selected for deletion depends on both its reward-cost ratio and a scale factor.

The scale factor differs by treap:

Treap Scale factor behavior
Pending Usually 1.0. Can become 0.0 depending on vote composition.
Pending votes Usually 1.0, unless the treap is more than 75% full, in which case it can become 0.0.
Penalty 1.0 while the treap has 100 or fewer transactions. Above that, it decreases according to the number of transactions.
Pending bundles Very large, because bundles are assumed to be high-reward.

For the penalty treap, the scale factor is:

scale factor = sqrt(100 / number of transactions in treap)

The scheduler multiplies the scale factor by the reward-to-cost ratio of sampled transactions, then deletes the transaction with the smallest result. A larger scale factor makes a transaction less likely to be deleted.

Transactions can also be dropped for other reasons. If the heap of the relevant Firedancer module is full, a new transaction is dropped if its priority is lower than the lowest-priority transaction already in the heap. Transactions can also be dropped if they require too many compute units or if their costs cannot be estimated.


Bundles are a special case. They can occupy only up to half the maximum pack depth. Once that threshold is reached, new bundles are dropped. Within the bundle treap, bundles are generally ordered first-in, first-out. Bundles also cannot be mixed with voting transactions, so they are scheduled only when no vote is scheduled.

There is one exception: initializer bundles. These are placed at the top of the list and executed next. They are used when state initialization is required at the beginning of a slot, and the relevant state may change between transactions.


Comparison of Solana scheduler implementations

Feature Agave central scheduler Agave greedy scheduler Firedancer / Frankendancer scheduler
Validator Agave Agave Firedancer / Frankendancer
Introduced in Agave v1.18 Agave v2.1 N/A; developed by Jump Crypto
Default? Yes, since Agave v2.0 No, opt-in Yes, balanced mode
Priority structure Priority queue → dependency graph Priority queue Treap / balanced binary search tree
Conflict handling Holds back conflicting transactions in the graph; schedules on the same thread when possible Completes and sends the current batch when a conflict is detected Uses penalty treap for hot accounts; bundles are generally FIFO
Batch strategy Tries to fill batches with non-conflicting high-priority transactions May release smaller batches to prioritize high-priority transactions sooner Configurable through balanced and perf modes
Transaction types Standard transactions Standard transactions Microblocks and bundles
Key trade-off Better batch construction, but graph construction is expensive under high load Faster handling of high-priority transactions, but smaller batches Revenue versus block fullness depends on configuration
Main weakness Dependency graph can be costly under load Smaller batches may reduce throughput Bundle expiry can invalidate the entire bundle

Summary

Solana scheduler design is still evolving. Agave moved from a thread-local multi-iterator model to a central scheduler, and later introduced the opt-in greedy scheduler. These changes show that scheduling is a hard problem: the scheduler must balance transaction priority, compute-unit usage, lock conflicts, batch size, validator revenue, and block fullness.


Firedancer takes a different approach, using treaps and configurable scheduling modes to balance performance and revenue. It also treats bundles as first-class scheduling objects, which introduces additional complexity around ordering, expiry, and block construction.

The main takeaway is that Solana scheduling is not a solved problem. It is an active design space where small policy differences can affect latency, throughput, validator economics, and user experience. With future protocol changes, such as Alpenglow, on the horizon, the Solana scheduler design will likely continue to evolve.

Acknowledgements

We are grateful for the support provided by the Staking Facilities team. We especially thank Matthias Schmitz for his valuable feedback on the report.

References

[1] Anza. Transaction Processing Unit in a Solana Validator.
[2] Andrew Fitzgerald. Solana Banking Stage and Scheduler.
[3] Anza. Introducing the Central Scheduler: An Optional Feature of Agave v1.18.
[4] Helius. All You Need to Know About Solana’s v1.18 Update.
[5] Helius. Agave v2.0 Update: All You Need to Know.
[6] Helius. Agave v2.1 Update: All You Need to Know.
[7] Harsh Patel. What’s new with Solana’s transaction scheduler?.
[8] Solana. Fee Structure.
[9] GETBLOCK. How To Optimize Solana Transactions with Priority Fees.
[10] Harsh Patel. What’s unique about Firedancer’s transaction scheduler?.
[11] Anza. agave., GitHub
[12] Firedancer-io. firedancer. GitHub

1 Like

Honestly, I think Agave is overcomplicating things. The central scheduler looks good on paper, but in practice we’re adding overhead for marginal gains. The greedy scheduler is more honest about what the network actually looks like under load. My concern is that we’re optimizing for congestion scenarios that rarely play out the way the models assume.