LLM Notice: This documentation site supports content negotiation for AI agents. Request any page with Accept: text/markdown or Accept: text/plain header to receive Markdown instead of HTML. Alternatively, append ?format=md to any URL. All markdown files are available at /md/ prefix paths. For all content in one file, visit /llms-full.txt
Skip to main content

Scheduled Transactions Performance Guide

Since the Forte network upgrade, scheduled transactions have been a powerful and useful feature for many developers in the Flow ecosystem, providing tools to automate a ton of functionality and have extremely useful features like on-chain cron jobs.

This feature comes with some things that developers have to consider though. The functionality is complex, and therefore can be expensive if not used properly. Many of the use cases for scheduled transactions are in the DeFi space, which has razor-thin margins. This means that you need to make sure your transactions are efficient as possible to save money on gas fees.

Biggest piece of advice: Stop calling FlowTransactionScheduler.estimate() just to get an estimate of the fees for the transaction! Use FlowTransactionScheduler.calculateFee() instead and save approximately 30 percent on computation.

The Problem: Double Work

Many developers are currently using this pattern when scheduling transactions:


_10
// Get an estimate of the fee required for the transaction
_10
// also does a lot of other things
_10
let estimate = FlowTransactionScheduler.estimate(...)
_10
let fee = estimate.fee
_10
_10
// withdraw the fee amount from the account's vault
_10
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
_10
_10
// schedule the transaction
_10
manager.schedule(txData, feeVault, ...)

What happens under the hood

estimate() performs these operation:

  • Validates transaction data
  • Calculates data size
  • finds an empty slot that the transaction will fit in
  • Computes fee

manager.schedule() also does:

  • Validates transaction data again
  • Calculates data size again
  • finds an empty slot that the transaction will fit in again
  • Computes the fee
  • Actually schedules the transaction

You are doing approximately 70 percent of the work in schedule() twice!

Computational Cost Comparison:

  • Old way: estimate() plus schedule() does double work because schedule() calls estimate!
  • New way: calculateFee() plus schedule() only calculates the fee twice, which is a trivial operation.
  • Result: ~30 percent reduction in computation

The Solution: Use FlowTransactionScheduler.calculateFee()

The new calculateFee() function does exactly one thing: calculates the fee.


_10
// Get an estimate of the fee required for the transaction
_10
let fee = FlowTransactionScheduler.calculateFee(...)
_10
_10
// withdraw the fee amount from the account's vault
_10
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
_10
_10
manager.schedule(txData, feeVault, ...)

Why this works: manager.schedule() does all the validation anyway, so you only need the fee upfront. Let schedule() handle the rest!

In the long run, this will save a TON on transaction fees, especially if your app is scheduling a lot of recurring transactions!

Bonus Optimization: Store Known Sizes

Scheduled Transactions can provide an optional piece of data when scheduling to be included with the transaction. The user must pay a fee for the storage of this data, so the contract needs to know its size.

If your transaction data is always the same size, stop calculating it every time!

Wasteful approach:


_10
// calculate the size of the data, which is an expensive operation
_10
let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)
_10
let fee = FlowTransactionScheduler.calculateFee(executionEffort, priority, dataSizeMB)

If the data that you are providing when scheduling is the same size every time, you can just store that size in a variable in your contract or somewhere else and just access that field when scheduling, instead of doing redundant operations to calculate the size every time.

Smart approach:


_10
// get the pre-set size of the data from a field in the contract
_10
let dataSizeMB = self.standardTxDataSizeMB
_10
let fee = FlowTransactionScheduler.calculateFee(executionEffort, priority, dataSizeMB)

Pro tip: If your scheduled transaction payload is standardized with same fields and similar values, calculate the size once and store it in a configurable field in your contract or resource.

Real World Examples

Before: Inefficient Code


_31
import FlowTransactionScheduler from 0x1234
_31
import FlowTransactionSchedulerUtils from 0x1234
_31
_31
transaction(
_31
txData: {String: AnyStruct},
_31
executionEffort: UInt64,
_31
priority: FlowTransactionScheduler.Priority
_31
) {
_31
prepare(acct: AuthAccount) {
_31
let manager = acct.borrow<&FlowTransactionSchedulerUtils.Manager>(
_31
from: FlowTransactionSchedulerUtils.ManagerStoragePath
_31
) ?? panic("No manager")
_31
_31
let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)
_31
let estimate = FlowTransactionScheduler.estimate(
_31
scheduler: manager.address,
_31
transaction: txData,
_31
...
_31
)
_31
let fee = estimate.fee
_31
_31
// withdraw the fee amount from the account's vault
_31
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
_31
_31
manager.schedule(
_31
transaction: txData,
_31
fee: <-feeVault,
_31
...
_31
)
_31
}
_31
}

After: Optimized Code


_31
import FlowTransactionScheduler from 0x1234
_31
import FlowTransactionSchedulerUtils from 0x1234
_31
import MyTransactionHandler from 0x5678
_31
_31
transaction(
_31
txData: {String: AnyStruct},
_31
executionEffort: UInt64,
_31
priority: FlowTransactionScheduler.Priority
_31
) {
_31
prepare(acct: AuthAccount) {
_31
let manager = acct.borrow<&FlowTransactionSchedulerUtils.Manager>(
_31
from: FlowTransactionSchedulerUtils.ManagerStoragePath
_31
) ?? panic("No manager")
_31
_31
let dataSizeMB = MyTransactionHandler.standardDataSize
_31
let fee = FlowTransactionScheduler.calculateFee(
_31
executionEffort: executionEffort,
_31
priority: priority,
_31
dataSizeMB: dataSizeMB
_31
)
_31
_31
// withdraw the fee amount from the account's vault
_31
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
_31
_31
manager.schedule(
_31
transaction: txData,
_31
fee: <-feeVault,
_31
...
_31
)
_31
}
_31
}

This is the fastest way if your transaction structure is consistent! Store the size in a configurable field instead of recalculating it every time.

When to still use estimate()

Use estimate() only when you need to validate the entire transaction before scheduling, such as in a UI where users need to see validation errors before submitting. For simple fee calculation, always use calculateFee().

Quick Reference

Do This:

  • Use FlowTransactionScheduler.calculateFee() for fee estimation
  • Store known data sizes in config fields
  • Let manager.schedule() handle validation and scheduling

Do Not Do This:

  • Call estimate() just for fees
  • Calculate size every transaction
  • Validate and schedule twice unnecessarily

Questions? Check out the Scheduled Transactions documentation at https://developers.flow.com/blockchain-development-tutorials/forte/scheduled-transactions/scheduled-transactions-introduction for more details.