Why this matters
When a client sends a transaction, two things need to happen: validate it and persist the results. In synchronous mode, both happen in the same request — the client waits until everything is written to the database before getting a response. That’s simple and predictable, but it has a ceiling. At high volumes, database writes become the bottleneck. Every transaction holds a connection, waits for locks, and competes for I/O. Async mode breaks that dependency. The transaction is validated, the response is returned immediately, and the persistence happens in the background through RabbitMQ. The client gets faster responses. The database gets writes in controlled, optimized batches. The system handles more with less. For broader scaling guidance, see Scalability strategies.
How it works
Synchronous mode (default)
The transaction is validated and written directly to PostgreSQL in the same request cycle. The API response is only sent after all database operations complete.
- The client sends a
POST /transactionto the Midaz API. - The API validates the request — DSL parsing, balance checks, and limit enforcement all happen here.
- The API writes to PostgreSQL — transaction, operations, and balance updates are persisted in the same request cycle.
- PostgreSQL confirms the write, acknowledging that all records are committed.
- The API returns
200 OKto the client with the created transaction. The response only leaves the server after the database has confirmed everything.
- Response time includes database write latency.
- Each transaction is an independent database operation.
- Simpler to reason about — what you see in the response is what’s persisted.
Asynchronous mode
The transaction is validated the same way, but instead of writing to the database, Midaz publishes a message to RabbitMQ. A background consumer picks up the message and handles persistence separately.
- The client sends a
POST /transactionto the Midaz API. - The API validates the request — DSL parsing, balance checks, and limit enforcement happen exactly as in synchronous mode.
- The API publishes the transaction payload to RabbitMQ instead of writing to the database directly.
- The API returns
200 OKto the client immediately after the message is accepted by the queue — the client doesn’t wait for database persistence. - RabbitMQ delivers the message to a background consumer, decoupled from the API request.
- The consumer writes to PostgreSQL — transaction, operations, and balance updates are persisted separately from the original request.
- Response time excludes database write latency — the client only waits for validation and queue publish.
- Messages are serialized with MessagePack for compact, efficient transport.
- Background consumers write to the database at their own pace, with batching and retry capabilities.
Built-in resilience
If RabbitMQ is unavailable when async mode tries to publish a message, Midaz doesn’t fail the transaction. Instead, it falls back automatically to a direct database write — the same path as synchronous mode. This means:
- No transaction is lost because of a queue outage.
- The client still gets a successful response.
- The fallback is logged so your operations team can investigate the queue issue.
Enabling async mode
Set one environment variable in the ledger application:
false (the default), all transactions use synchronous processing. No RabbitMQ consumer is needed.
When set to true, the ledger publishes transaction payloads to the configured RabbitMQ exchange and a background consumer handles persistence.
RabbitMQ configuration
Async mode uses the following RabbitMQ settings (all in the ledger
.env):
| Variable | Description | Default |
|---|---|---|
RABBITMQ_TRANSACTION_ASYNC | Enable async processing. | false |
RABBITMQ_HOST | RabbitMQ server hostname. | midaz-rabbitmq |
RABBITMQ_PORT_HOST | Management API port. | 3003 |
RABBITMQ_PORT_AMQP | AMQP protocol port. | 3004 |
RABBITMQ_DEFAULT_USER | Producer credentials (user). | transaction |
RABBITMQ_DEFAULT_PASS | Producer credentials (password). | — |
RABBITMQ_CONSUMER_USER | Consumer credentials (user). | consumer |
RABBITMQ_CONSUMER_PASS | Consumer credentials (password). | — |
RABBITMQ_NUMBERS_OF_WORKERS | Number of consumer worker goroutines. | 5 |
RABBITMQ_NUMBERS_OF_PREFETCH | Messages prefetched per worker. | 10 |
RABBITMQ_TRANSACTION_BALANCE_OPERATION_EXCHANGE | Exchange name for transaction messages. | transaction.transaction_balance_operation.exchange |
RABBITMQ_TRANSACTION_BALANCE_OPERATION_KEY | Routing key. | transaction.transaction_balance_operation.key |
RABBITMQ_TRANSACTION_BALANCE_OPERATION_QUEUE | Queue name. | transaction.transaction_balance_operation.queue |
Balance synchronization
When running in async mode, balance updates are coordinated through a dedicated synchronization worker that uses Redis as a coordination layer. This ensures balances remain consistent even when multiple consumers process messages concurrently.
| Variable | Description | Default |
|---|---|---|
BALANCE_SYNC_BATCH_SIZE | Number of balance updates to batch before flushing. | 50 |
BALANCE_SYNC_FLUSH_TIMEOUT_MS | Maximum wait time (ms) before flushing an incomplete batch. | 500 |
BALANCE_SYNC_POLL_INTERVAL_MS | How often (ms) the worker checks for pending updates. | 50 |
RabbitMQ circuit breaker
When async mode is enabled, Midaz depends on RabbitMQ for transaction persistence. To protect against broker outages, Midaz includes a built-in circuit breaker that monitors the health of the RabbitMQ connection and fails fast when the broker is unavailable — preventing request pileups and cascading failures. The circuit breaker is always active when RabbitMQ is in use. It follows the standard three-state model:
- Closed (normal): requests flow through to RabbitMQ. Failures are counted.
- Open (tripped): requests are rejected immediately without contacting RabbitMQ. A background health checker monitors the broker and attempts recovery.
- Half-open (probing): a limited number of requests are allowed through to test if RabbitMQ has recovered. If they succeed, the circuit closes. If they fail, it reopens.
- The number of consecutive failures reaches the threshold, OR
- The failure ratio exceeds the configured percentage within the counting window
Circuit breaker configuration
| Variable | Description | Default |
|---|---|---|
RABBITMQ_CIRCUIT_BREAKER_CONSECUTIVE_FAILURES | Consecutive failures before the circuit opens. | 15 |
RABBITMQ_CIRCUIT_BREAKER_FAILURE_RATIO | Failure percentage (0–100) that triggers open state. | 50 |
RABBITMQ_CIRCUIT_BREAKER_MIN_REQUESTS | Minimum requests before evaluating the failure ratio. | 10 |
RABBITMQ_CIRCUIT_BREAKER_INTERVAL | Time window (seconds) for counting failures. Counters reset after each interval. | 120 |
RABBITMQ_CIRCUIT_BREAKER_TIMEOUT | How long (seconds) the circuit stays open before transitioning to half-open. | 30 |
RABBITMQ_CIRCUIT_BREAKER_MAX_REQUESTS | Requests allowed through in half-open state to probe recovery. | 3 |
RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_INTERVAL | How often (seconds) the background health checker pings RabbitMQ. | 30 |
RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_TIMEOUT | Timeout (seconds) for each health check ping. | 10 |
When the circuit is open, async transactions fall back to direct synchronous database writes — the transaction is not lost. This fallback ensures data integrity even during broker outages.
How async mode connects to Bulk Recorder
Async mode and the Bulk Recorder are complementary features that 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 — batching multiple messages into single bulk inserts.
RABBITMQ_TRANSACTION_ASYNC=true AND BULK_RECORDER_ENABLED=true). Without async mode, there’s no message queue — and without a queue, there’s nothing to batch.
| Configuration | Processing behavior |
|---|---|
Async false | Direct database write per transaction (synchronous) |
Async true, Bulk Recorder false | Queue-based, one message processed at a time |
Async true, Bulk Recorder true | Queue-based, messages batched for bulk inserts (10×+ throughput) |
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’re running batch operations like mass payouts or settlements.
- You want to decouple your API tier from database performance.
- You need the simplest possible setup (no RabbitMQ dependency).
- Transaction volume is low to moderate.
- You want the guarantee that a successful API response means the data is already persisted.
- You’re in a development or testing environment where simplicity matters more than throughput.

