How to Retry an Energy Rental API Request Without Paying Twice
Oliver BentleyTRON Research EditorShort answer: in TRON, idempotency is guaranteed not by an HTTP header but by the txID of the signed transaction. If you've stored the signed payload and its txID, retrying the request with the same payload is safe — the network deduplicates the transaction and won't execute it twice. Double charges don't come from retrying; they come from rebuilding. Any change to the transaction body, down to reordering fields, yields a new txID — that is, a new, separately billed delegation operation.
Two levels of idempotency in energy rental
The network level. Energy rental is a DelegateResourceContract: the resource owner delegates energy to a recipient address, and the recipient uses that quota without staking anything themselves. The transaction parameters you're retrying are: resource (Bandwidth or Energy only), the amount (minimum 1 TRX, i.e. 1,000,000 sun), the recipient (not your own address and not a contract), and the optional lock_period. Important: lock_period is measured in blocks, not seconds — at a 3-second block interval, 86,400 blocks ≈ 3 days, while the current maximum is set by network parameter #78 and can be queried via wallet/getchainparameters. Never hardcode the period: if the parameter changes, so does the transaction body (delegateresource).
The provider's business API level. Here idempotency relies on your own order identifier: an external order_id, unique in your database and passed with every retry. This is a general pattern for reliable write operations, not documented behavior of any particular rental service: a repeat commit with the same key should return the already-recorded entry rather than create a new one. If the provider's API doesn't offer such a field, you can't retry provisioning requests automatically — only through manual reconciliation against the provisioning history.
Retries: what's safe and what isn't
| Operation | Safe to auto-retry |
|---|---|
estimateenergy, triggerconstantcontract, cost calculation | Yes, read-only |
Reading order status, gettransactionbyid | Yes |
| Broadcasting the same signed payload | Yes, the txID doesn't change |
Creating a new order without order_id | No |
| Rebuilding and re-signing the transaction | Only after verification (see below) |
The rule is simple: reads can be retried freely; remote writes are never retried automatically until idempotency has been proven.
Why 200 OK, result: true, and "duplicate" prove nothing
result: true in a broadcast response only means the call completed without a reported error. It does not prove that the transaction stayed in the pending pool, propagated across the network, made it into a block, executed successfully, and was finalized. Finality is verified via /walletsolidity/gettransactionbyid and gettransactioninfobyid; success means the top-level result is not FAILED and receipt.result = SUCCESS.
The second trap: if an exception occurs during processing, the HTTP status may still be 200 while the body contains a single Error field. A retry layer that makes decisions based solely on HTTP codes will let such a response slip through.
The third is the least intuitive. DUP_TRANSACTION_ERROR usually means the same txID is already in the node's pending pool or in a block, but it does not prove inclusion in the chain: with node.rpc.trxCacheEnable turned on, the only match may have been the broadcast RPC cache. If you get this error, don't mark the order as successful — query the transaction and receipt by txID (broadcast errors).
When you may build a replacement transaction
By default, raw_data.expiration is the construction time plus 60 seconds (24 hours maximum), so "60 seconds" isn't a universal constant. And more importantly: your local clock passing expiration is not proof of non-inclusion.
- Wait until the timestamp of the latest solidified block on a reliable, synced node is ≥ the original transaction's
expiration. A block is considered solidified once at least 19 active SRs have alatestBlockNumno lower than its number — typically about a minute. - Query the original txID on the solidity endpoint. An empty response proves nothing on its own: the node may not have finalized the relevant block yet — the outcome remains unknown and polling must continue.
- Only once you've confirmed the txID is absent from the solidified canonical chain should you rebuild the transaction from a fresh TAPOS reference. The old txID is not reused.
Immediate rebuilding is acceptable in exactly one case: the transaction was never sent to any node and expired before the first broadcast attempt. If it was sent, or the outcome is unknown, you go with a safe re-broadcast.
Transport-level retries under load
On 429 and 403, retry with exponential backoff and jitter — otherwise several service instances will retry in lockstep. On 403, first check the API key and a possible temporary block. Honor the Retry-After header if present. Design polling around block production: a new block appears roughly every 3 seconds, and polling more often is pointless (rate limits).
Implementation checklist
- Estimate energy via
estimateenergy/triggerconstantcontractbefore broadcasting:fee_limit (sun) = energy_required × getEnergyFeeplus a buffer. - Store the signed payload and txID before the first send — that's your idempotency key.
- For the provider's API, generate
order_idon your side and pass it with every retry. - Separate retryable and non-retryable operations; set the "success" status only on solidified confirmation.
- Introduce an "unknown" status — without it, the system will start spawning replacements and paying twice.
Conclusion
Idempotency in TRON energy rental rests on two identifiers: the txID at the network level and your order_id at the API level. Every double charge begins the moment a service decides "it probably didn't go through" — which is why that decision must be based on the chain's solidified state, not on a timeout, an HTTP code, or the word "duplicate" in an error message.