OUT OF ENERGY in Automated Payouts: How to Retry Transactions Without Double-Spending

September 26, 2026
Chloe MeilinTRON infrastructure analyst
Short answer

OUT_OF_ENERGY is an execution failure of a transaction that has already been included in a block: the txID exists, the state changes are rolled back, the USDT was never transferred, but the resource was consumed. That is why you may only retry a payout after confirming via the receipt that the result is not success and that the original txID is absent from the solidified chain.

If your gateway receives OUT_OF_ENERGY, you can retry the payout — but only after checking the status by txID. This error occurs after the transaction has been included in a block: the transaction is on-chain, the state changes are rolled back, the USDT never left, and Energy was consumed. Double-spending arises not from the error itself but from a blind retry issued while the outcome of the first attempt is still undetermined.

Why OUT_OF_ENERGY on its own never causes a double payout

In TRON's documentation, TVM execution failures (REVERT, OUT_OF_ENERGY, OUT_OF_TIME) form a separate category — they are not broadcast errors. The transaction is on the blockchain, but that does not mean it executed successfully: when an exception occurs, no commit happens and state changes are not applied. For a USDT TRC-20 transfer, this means the recipient's balance did not change.

The success criterion for a payout is not "broadcast returned true" and not "the txID is on-chain", but the execution result:

  • transactionInfo.receipt.result == success via /walletsolidity/gettransactioninfobyid;
  • or transaction.ret.contractRet == success via /walletsolidity/gettransactionbyid.

If a payout is marked as "sent" based on the broadcast response, that is the first source of accounting discrepancies.

Where double-spending actually comes from

There is exactly one dangerous state: the outcome is unknown. The node accepted the transaction into its local mempool and returned {"result": true}, but it never reached the producing SR — and gettransactioninfobyid keeps returning empty. An empty response here is not proof of non-execution: the transaction may still make it into a block.

The official recommendation is to confirm, before creating a replacement, that the original txID is absent from the solidified chain, and to rely on the expiration window. A transaction expires roughly 60 seconds after it is built, and the deadline is tied to the head block's timestamp rather than the node's clock; the gap between expiration and timestamp can drift, so do not hardcode expiration − timestamp == 60 000.

Decision matrix for a payout worker

Observed stateAction
receipt.result == successpayout complete, retry forbidden
receipt present, result == OUT_OF_ENERGYfunds did not move; a new transaction with a corrected budget may be created
broadcast ok, receipt empty, not yet expiredwait; re-broadcasting the same payload is acceptable
expired, txID absent from the solidified chainrebuild the transaction with a fresh TAPOS reference
broadcast never went through on any node, expiredrebuild and re-sign

Retrying a transaction: two different mechanisms

1. Re-broadcasting the same signed payload. While the transaction is still valid, the same payload can be broadcast again to the original or another synchronized node: the txID stays the same, network-level deduplication prevents repeated execution, and you may get DUP_TRANSACTION_ERROR in response. This cannot result in two transfers.

2. Rebuilding a new transaction. Once the transaction has expired, the old payload is "dead" and the txID cannot be reused: you need a new TAPOS reference, signature and txID. Here, preventing duplicates is your responsibility: one logical payment is bound to the list of every txID issued for it, and a new attempt is created only after the checks from the table above. Application-level idempotency (payout keys, database statuses, locks) is not covered by the documentation — the network only provides txID deduplication and receipts. If the reference block ended up on a fork or aged beyond the 65,536-block window, validation will fail with TaposException and the transaction will never reach the chain.

Diagnostics and ruling out unrelated errors

The cause is read from resMessage in gettransactioninfobyid after converting hex to a string ("Not enough energy for 'AND' operation executing: curInvokeEnergyLimit[1000], curOpEnergy[3], usedEnergy[1000]"); the error code can also be obtained from the contractResult field.

  • OUT_OF_ENERGY and OUT_OF_TIME are often confused: the latter relates to the execution time limit, fires intermittently due to varying SR performance, and consumes the entire fee_limit. Look at the exact message, not the code.
  • SERVER_BUSY appears when a node has more than 2,000 unprocessed transactions — it calls for a retry with backoff or switching endpoints. Separately, apply exponential backoff with jitter on HTTP 429, otherwise your worker instances will retry in lockstep.
  • The top-level code on a failed broadcast only means "validation did not pass" — the real reason lies in the hex message field.

Exactly how much resource is burned on failure is not stated unambiguously in the documentation: wordings range from "the Energy already consumed is charged, up to the limit" to "it exhausts the entire allowance". Plan for the worst case.

How to reduce the failure rate

A USDT transfer to an address with a non-zero token balance costs on the order of 64,000 Energy; to an address with a zero balance, on the order of 130,000. These figures are indicative and depend on the contract's energy_factor and network conditions. Under the dynamic Energy model, exceeding the consumption threshold multiplies the effective cost by a factor of up to 4.4× at current network parameters, so a budget calculated on a "normal" day breaks under load.

  • keep a conservative buffer at the upper bound, or query the contract's energy_factor once per maintenance period — this is a budget, not a guarantee, as the documentation states outright;
  • take getMaxFeeLimit and other limits from wallet/getchainparameters rather than a hardcoded constant: old figures such as "1000 TRX maximum" are outdated;
  • run a pre-check via triggerconstantcontract — local execution with no on-chain transaction and no resource consumption;
  • remember that the maximum Energy that can be covered is the minimum of fee_limit / EnergyPrice and the available Energy plus whatever can be purchased with the TRX balance: a sufficient balance alone is not enough;
  • factor the activation of new recipients into your cost base: +25,000 Energy when activation happens via a contract call.

An Energy pool for payouts is built up by staking TRX, delegating via DelegateResourceContract, or renting on the JustLend DAO market.

Conclusion

A double-spend-free scheme rests on three rules: success is confirmed only by the receipt; retries are forbidden while the outcome is unknown, until expiration and a solidified-chain check; within the validity window the same payload is re-broadcast, and after it, a new transaction is created and bound to the same logical payment. Then OUT_OF_ENERGY becomes an ordinary failure with a known price tag rather than a source of balance discrepancies.