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_10let estimate = FlowTransactionScheduler.estimate(...)_10let fee = estimate.fee_10_10// withdraw the fee amount from the account's vault_10let feeVault <- authorizerVaultRef.withdraw(amount: fee)_10_10// schedule the transaction_10manager.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()plusschedule()does double work becauseschedule()calls estimate! - New way:
calculateFee()plusschedule()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_10let fee = FlowTransactionScheduler.calculateFee(...)_10_10// withdraw the fee amount from the account's vault_10let feeVault <- authorizerVaultRef.withdraw(amount: fee)_10_10manager.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_10let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)_10let 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_10let dataSizeMB = self.standardTxDataSizeMB_10let 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
_31import FlowTransactionScheduler from 0x1234_31import FlowTransactionSchedulerUtils from 0x1234_31_31transaction(_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
_31import FlowTransactionScheduler from 0x1234_31import FlowTransactionSchedulerUtils from 0x1234_31import MyTransactionHandler from 0x5678_31_31transaction(_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.