Why this matters
A client sends a transaction. Midaz must do two things: validate it and persist the result. In synchronous mode, both steps run in the same request. The client waits for every write to reach the database before it gets a response. This model is simple and predictable, but it has a ceiling. At high volume, database writes become the bottleneck. Each transaction holds a connection, waits for locks, and competes for I/O. Async mode breaks that dependency. Midaz validates the transaction, returns the response at once, and persists the data in the background through RabbitMQ. The client gets faster responses. For broader scaling guidance, see Scalability strategies.
How it works
Synchronous mode (default)
Midaz validates the transaction and writes it directly to PostgreSQL in the same request cycle. The API sends its response only after every database operation completes.Figure 1. Synchronous transaction flow. The client waits until the database write is confirmed.
-
The client sends a
POST /transactionto the Midaz API. - The API validates the request. It runs request-shape validation, balance checks, and limit enforcement here.
- The API writes to PostgreSQL. It persists the transaction and its operations in the same request cycle.
- PostgreSQL confirms the write. It commits all records.
-
The API returns
201 Createdto the client with the created transaction. The response carries theCREATEDstatus, and Midaz stores the transaction asAPPROVED. A201means Midaz applied the transaction to balances.
- Response time includes database write latency.
- Each transaction is an independent database operation.
- Simpler to reason about.
Even in synchronous mode, Midaz updates balances atomically in Redis during the request. Redis is authoritative for balances. The write above persists the transaction and its operations, not the Postgres balance rows. The always-on balance-sync worker reconciles those rows (see Balance synchronization).
Asynchronous mode
Midaz validates the transaction the same way. Instead of a direct database write, Midaz publishes a message to RabbitMQ. A background consumer picks up the message and handles persistence separately.Figure 2. Asynchronous transaction flow. The client receives a response as soon as the message is published, and persistence happens in the background.
-
The client sends a
POST /transactionto the Midaz API. - The API validates the request. It runs request-shape validation, balance checks, and limit enforcement exactly as in synchronous mode.
- The API publishes the transaction payload to RabbitMQ instead of a direct database write.
-
The API returns
201 Createdto the client as soon as the queue accepts the message. The client does not wait for database persistence. The response carries theCREATEDstatus, and Midaz stores the transaction asAPPROVED. A201means Midaz applied the transaction to balances. - RabbitMQ delivers the message to a background consumer, decoupled from the API request.
- The consumer writes to PostgreSQL. It persists the transaction and its operations from the queued message. The balance-sync worker copies balances to PostgreSQL in both modes (see the Balance synchronization section).
- Response time excludes database write latency.
- List queries show a new transaction after the consumer writes it.
- Background consumers write to the database at their own pace, with retries. Batch inserts require an enabled Bulk Recorder.
Built-in resilience
If the publish to RabbitMQ fails in async mode, Midaz writes the transaction to the database in the request. If that write also fails, the transaction stays applied to balances. Midaz keeps the transaction record and retries the write in the background. Synchronous mode uses the same recovery when its database write fails. This means:
- During a queue outage, Midaz writes directly to the database.
- A failed database write does not reject a transaction that Midaz already applied to balances.
- Midaz logs the queue failure and the deferred write so your operations team can investigate them.
Enabling async mode
Set one environment variable in the ledger application:
false (the default), all transactions use synchronous processing and persist directly to PostgreSQL. The ledger still connects to RabbitMQ and runs its consumer in this mode.
With true, the ledger publishes transaction payloads to the configured RabbitMQ exchange. A background consumer then handles persistence.
RabbitMQ configuration
Async mode uses the following RabbitMQ settings (all in the ledger
.env):
Balance synchronization
A dedicated balance-sync worker copies balances from Redis to PostgreSQL. This worker runs in both synchronous and asynchronous modes.
The balance-sync worker runs automatically in both modes. You need no extra setup beyond an available Redis instance.
RabbitMQ circuit breaker
A built-in circuit breaker protects the RabbitMQ publishes of audit messages, overdraft events, and annotation transactions. It monitors the health of the RabbitMQ connection and fails fast when the broker goes down. This prevents request pileups and cascading failures. The circuit breaker is active on the RabbitMQ path. The circuit breaker follows the standard three-state model:
- Closed (normal): requests flow through to RabbitMQ. The breaker counts failures.
- Open (tripped): the breaker does not contact RabbitMQ. An annotation transaction goes straight to the database, and Midaz returns an error if that write fails. A background health checker monitors the broker and attempts recovery.
- Half-open (probing): the breaker lets a limited number of requests through to test RabbitMQ recovery. If they succeed, the circuit closes. If they fail, it reopens.
- The number of consecutive failures reaches the threshold, OR
- The failure ratio reaches the configured percentage within the counting window
Circuit breaker configuration
How async mode connects to Bulk Recorder
Async mode and the Bulk Recorder work together:
- Async mode decouples the API response from persistence. Transactions go to RabbitMQ instead of directly to PostgreSQL.
- Bulk Recorder optimizes how the consumer writes those messages to the database. It batches multiple messages into single bulk inserts.
BULK_RECORDER_ENABLED=false. Without async mode, transaction persistence uses direct database writes and there is no transaction queue to batch.
When to use async mode
Use async mode when:
- You need lower API response times for transaction creation.
- Your workload involves high transaction volumes (hundreds+ per second).
- You run batch operations like mass payouts or settlements.
- You want to decouple your API tier from database performance.
- You need direct, request-bound transaction persistence.
- Transaction volume is low to moderate.
- You work in a development or testing environment where simplicity matters more than throughput.

