Skip to main content

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.
Sequence diagram showing the client sending a POST /transaction to the Midaz API, which validates it, writes the transaction and operations to PostgreSQL, waits for confirmation, and only then returns 201 Created to the client.

Figure 1. Synchronous transaction flow. The client waits until the database write is confirmed.

Here’s the full flow, step by step:
  1. The client sends a POST /transaction to the Midaz API.
  2. The API validates the request. It runs request-shape validation, balance checks, and limit enforcement here.
  3. The API writes to PostgreSQL. It persists the transaction and its operations in the same request cycle.
  4. PostgreSQL confirms the write. It commits all records.
  5. The API returns 201 Created to the client with the created transaction. The response carries the CREATED status, and Midaz stores the transaction as APPROVED. A 201 means Midaz applied the transaction to balances.
Characteristics:
  • 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.
Sequence diagram showing the client sending a POST /transaction to the Midaz API, which validates it, publishes the payload to RabbitMQ, and immediately returns 201 Created to the client. In parallel, RabbitMQ delivers the message to a background consumer, which writes the transaction and operations to PostgreSQL; balances are handled by the dedicated balance-sync worker.

Figure 2. Asynchronous transaction flow. The client receives a response as soon as the message is published, and persistence happens in the background.

Here’s the full flow, step by step:
  1. The client sends a POST /transaction to the Midaz API.
  2. The API validates the request. It runs request-shape validation, balance checks, and limit enforcement exactly as in synchronous mode.
  3. The API publishes the transaction payload to RabbitMQ instead of a direct database write.
  4. The API returns 201 Created to the client as soon as the queue accepts the message. The client does not wait for database persistence. The response carries the CREATED status, and Midaz stores the transaction as APPROVED. A 201 means Midaz applied the transaction to balances.
  5. RabbitMQ delivers the message to a background consumer, decoupled from the API request.
  6. 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).
Characteristics:
  • 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.
The validation step is identical in both modes. Balance checks, request-shape validation, and limit enforcement all happen before the API responds, regardless of processing mode. The difference is only in when the data hits the database.

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.
During a queue outage, latency can increase because writes go straight to the database. Monitor your RabbitMQ health to keep async mode active.

Enabling async mode


Set one environment variable in the ledger application:
With 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):
The consumer uses separate credentials (RABBITMQ_CONSUMER_USER / RABBITMQ_CONSUMER_PASS) from the producer. The consumer republishes the messages that it retries, so it also needs write access to the transaction exchange.

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 circuit opens when either condition is true:
  • The number of consecutive failures reaches the threshold, OR
  • The failure ratio reaches the configured percentage within the counting window

Circuit breaker configuration

For most production deployments, the defaults work well. Tune CONSECUTIVE_FAILURES and TIMEOUT if your RabbitMQ cluster has known recovery patterns. For example, lower the timeout if your broker recovers within seconds. Increase consecutive failures if you see transient network blips.

How async mode connects to Bulk Recorder


Async mode and the Bulk Recorder work together:
  1. Async mode decouples the API response from persistence. Transactions go to RabbitMQ instead of directly to PostgreSQL.
  2. Bulk Recorder optimizes how the consumer writes those messages to the database. It batches multiple messages into single bulk inserts.
Bulk Recorder is active with async mode unless you explicitly set 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.
Keep synchronous mode when:
  • 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.
You can switch between modes at any time. Change RABBITMQ_TRANSACTION_ASYNC and restart the ledger application. You need no data migration, because the transaction format is the same in both paths.