Retries and idempotency
Learn how to implement retries safely, handle timeouts, and avoid duplicate operations using identification, validation, and concurrency control.
Temporary failures are part of any integration. Timeouts, network instability, and inconclusive responses can happen even when the operation was processed correctly.
The risk arises when the application repeats an operation without knowing whether the previous attempt has already taken effect. In financial flows, uncontrolled retries can cause duplicates and inconsistencies.
By the end of this page, you will understand when to repeat an operation, how to differentiate temporary failures from definitive ones, and how to reduce the risk of duplicates using identification, validation, and concurrency control.
When to use
Apply these practices when your integration needs to:
- repeat calls after a timeout or instability;
- recover operations with an inconclusive result;
- execute automatic retries;
- avoid duplicate creation of entities;
- process operations in queues or workers;
- ensure that the same business intent is not executed more than once.
Before you start
Before implementing retries:
- define which errors may justify a new attempt;
- assign a unique internal identifier to each operation;
- link this identifier to the ID returned by Asaas;
- use
externalReferencewhen the resource used provides this field and it is suitable for the flow; - define how your application will check whether a previous attempt has already taken effect;
- implement controls to prevent concurrent executions of the same operation.
How it works
A communication failure does not automatically mean the operation failed.
Before repeating a call, your application must first determine whether the previous attempt may have been processed.
%%{init: {"flowchart": {"nodeSpacing": 28,"rankSpacing": 32,"diagramPadding": 8,"padding": 10}}}%%
flowchart TD
A["Execute operation"] --> B{"Conclusive response?"}
B --> BSim(("Yes"))
B --> BNao(("No"))
BSim --> C["Record result"]
BNao --> D["Record inconclusive attempt"]
D --> E{"Does the operation already exist?"}
E --> ESim(("Yes"))
E --> ENao(("No"))
ESim --> F["Use existing operation"]
ENao --> G{"Does the error allow a new attempt?"}
G --> GSim(("Yes"))
G --> GNao(("No"))
GSim --> H["Wait for backoff"]
H --> I["Execute new attempt"]
I --> B
GNao --> J["Record definitive failure"]
classDef inicio fill:#DBEAFE,stroke:#2563EB,color:#1E3A8A,stroke-width:3px,font-size:17px
classDef processo fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E,stroke-width:2px,font-size:17px
classDef decisao fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px,font-size:17px
classDef sucesso fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:3px,font-size:17px
classDef recuperacao fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-width:2px,font-size:17px
classDef respostaSim fill:#22C55E,stroke:#15803D,color:#FFFFFF,stroke-width:3px
classDef respostaNao fill:#EF4444,stroke:#B91C1C,color:#FFFFFF,stroke-width:3px
class A inicio
class B,E,G decisao
class C,F sucesso
class D,H,I processo
class J recuperacao
class BSim,ESim,GSim respostaSim
class BNao,ENao,GNao respostaNao
linkStyle default stroke:#94A3B8,stroke-width:2px
The retry should be the last step of the decision, not the first. Before executing a new attempt, your application needs to evaluate the previous result and confirm whether repeating the operation is really necessary.
1. Do not treat a timeout as a confirmed failure
When a call times out or the connection is interrupted, your application only knows that it did not receive a conclusive response.
This does not necessarily mean the operation was not processed.
A charge, transfer, subscription, or other entity may have been created in Asaas even if the response did not reach your application.
That is why the flow should not be:
timeout → repeat immediately
The flow should consider:
timeout → check the previous result → decide whether a repeat is necessary
Do not assume an operation failed just because your application did not receive the response. Repeating a creation without checking the previous result may create a new entity for the same business intent.
2. Differentiate temporary failures from definitive failures
Not every error should trigger a retry.
Before repeating a call, classify the type of failure that occurred.
| Situation | Handling |
|---|---|
| Timeout or connection failure | Consider the result inconclusive and check whether the operation has already been executed before repeating |
| Temporary server error | May justify a new attempt after validation and an appropriate wait |
| Temporary request limit | Wait before trying again and respect the applicable limits |
| Validation error | Fix the data before sending a new request |
| Business rule rejected | Evaluate the cause before deciding on a new attempt |
| Authentication or authorization error | Fix the configuration or credential before repeating |
Repeating the same request without changing the cause of a definitive failure only produces the same error again.
3. Uniquely identify each operation
Your application must be able to recognize the business intent that originated a call.
Create a unique internal identifier before the first attempt and keep that identifier throughout the entire retry cycle.
This link makes it possible to answer questions such as:
- has this operation already been sent?
- is there an attempt in progress?
- has Asaas already created the corresponding entity?
- which Asaas ID is related to this operation?
- how many attempts have already occurred?
- what was the result of the last attempt?
The identifier must exist before the call, not only after a successful response.
4. Use externalReference when applicable
externalReference when applicableWhen the resource used provides externalReference, the field can help link the record created in Asaas to the existing operation in your system.
For example:
{
"customer": "cus_000000000000",
"billingType": "PIX",
"value": 100,
"dueDate": "2026-09-30",
"externalReference": "order-98765"
}The value used must be stable for the same operation.
Avoid generating a new externalReference on each retry, as this eliminates precisely the relationship needed to recognize different attempts of the same intent.
Generate the operation identifier before the first call and reuse it in all attempts related to the same business intent.
5. Link the internal identifier to the Asaas ID
When a call returns successfully, store the relationship between:
- the internal identifier of the operation;
externalReference, when used;- the ID returned by Asaas;
- the result of the attempt;
- date and time;
- the state known at that moment.
This relationship must be kept even after the operation is completed.
It will be useful for:
- later queries;
- Webhook processing;
- reconciliation;
- failure investigation;
- preventing new unintended attempts.
6. Check the result before repeating
When an attempt has an inconclusive result, try to determine whether the previous operation has already taken effect.
The recommended flow is:
%%{init: {"flowchart": {"nodeSpacing": 28,"rankSpacing": 32,"diagramPadding": 8,"padding": 10}}}%%
flowchart TD
A["Attempt with inconclusive result"] --> B["Retrieve operation identifier"]
B --> C["Check local records"]
C --> D{"Is there an associated Asaas ID?"}
D --> DSim(("Yes"))
D --> DNao(("No"))
DSim --> E["Query or use existing operation"]
DNao --> F["Check whether the operation was created"]
F --> G{"Operation found?"}
G --> GSim(("Yes"))
G --> GNao(("No"))
GSim --> H["Associate the found ID"]
H --> E
GNao --> I["Evaluate new attempt"]
classDef inicio fill:#DBEAFE,stroke:#2563EB,color:#1E3A8A,stroke-width:3px,font-size:17px
classDef processo fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E,stroke-width:2px,font-size:17px
classDef decisao fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px,font-size:17px
classDef sucesso fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:3px,font-size:17px
classDef recuperacao fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-width:2px,font-size:17px
classDef respostaSim fill:#22C55E,stroke:#15803D,color:#FFFFFF,stroke-width:3px
classDef respostaNao fill:#EF4444,stroke:#B91C1C,color:#FFFFFF,stroke-width:3px
class A inicio
class B,C,F processo
class D,G decisao
class E,H sucesso
class I recuperacao
class DSim,GSim respostaSim
class DNao,GNao respostaNao
linkStyle default stroke:#94A3B8,stroke-width:2px
If the operation already exists, use the record found instead of creating a new one.
Only proceed with a new attempt when there is enough evidence to conclude that the previous operation did not produce the expected effect.
7. Control concurrent retries
Even when using a unique identifier, your application can still create duplicates if several processes execute the same operation simultaneously.
This can happen, for example, when:
- two workers receive the same task;
- a message is delivered more than once by a queue;
- the user submits the same action repeatedly;
- two servers process the same operation;
- a retry starts while the previous attempt is still in progress.
In this scenario, two processes may check almost at the same time that the operation does not exist yet and both execute the creation.
Implement a mechanism that ensures only one process moves forward with the same operation at a time.
Depending on the architecture, this can be done with:
- a distributed lock;
- transactional control;
- a unique constraint in the database;
- an internal processing state;
- serialization by business key.
The mechanism chosen depends on your application's infrastructure. The requirement is to prevent two concurrent executions from producing the same effect.
8. Apply progressive waits between attempts
Immediate back-to-back retries increase the load on the integration and may repeat calls while the cause of the failure is still present.
Use a backoff strategy, increasing the interval between attempts.
For example:
1st retry: wait 1 second
2nd retry: wait 2 seconds
3rd retry: wait 4 seconds
4th retry: wait 8 secondsThe values should be adjusted to the behavior of the operation and the limits applicable to the API.
Also set a maximum number of attempts. An operation should not remain in retry indefinitely.
9. Record all attempts
Each attempt should leave enough information to reconstruct what happened.
Record, when applicable:
- the internal identifier of the operation;
- the Asaas ID, when available;
- the attempt number;
- date and time;
- the HTTP result;
- the failure classification;
- the decision made after the error;
- the interval applied before the next attempt;
- the final processing state.
This data makes it possible to differentiate an isolated instability from a recurring problem in the integration.
Attention — duplicates and common mistakes
Avoid implementations that:
- treat a timeout as confirmation that the operation did not occur;
- repeat a creation immediately after losing the response;
- retry on any type of error;
- repeat validation failures without fixing the payload;
- do not have an internal identifier per operation;
- generate a new identifier on each attempt;
- do not link the local record to the Asaas ID;
- search for operations only by fragile combinations such as amount, date, and customer;
- allow multiple processes to repeat the same operation simultaneously;
- execute attempts back to back without an interval;
- retry indefinitely.
Retry does not mean repeating the call automatically. It means safely recovering an operation after determining what happened in the previous attempt.
Confirm your architecture
Before go-live, confirm that your application can answer:
- Which errors can trigger a new attempt?
- Which errors require a fix before any retry?
- What happens when a call ends in a timeout?
- Is there a unique identifier created before the first attempt?
- Does this identifier remain the same during retries?
- Is the ID returned by Asaas associated with the internal operation?
- Is it possible to check whether the previous operation has already taken effect?
- Does the system prevent two simultaneous executions of the same operation?
- Is there a progressive wait between attempts?
- Is there a maximum retry limit?
- Are all attempts recorded for diagnosis?
If these conditions are not met, an attempt created to recover from a failure may cause an additional problem: re-executing an operation that had already been completed.
Best practices
- treat a timeout as an inconclusive result, not as a confirmed failure;
- classify the error before deciding on a retry;
- create the internal identifier before the first attempt;
- use
externalReferencewhen the resource provides this field and it makes sense for the flow; - keep the same identifier during all attempts of the operation;
- store the ID returned by Asaas as soon as it is available;
- check the previous result before creating a new operation;
- protect the flow against concurrent executions;
- use backoff between attempts;
- set a maximum retry limit;
- keep enough history to diagnose failures and duplicates.
Next steps
After structuring retries and mechanisms to prevent duplicates, validate that all components of the integration are ready to operate in the production environment:
Updated 2 days ago
