Out of the box, a default Mirth Connect installation handles approximately 500 messages per hour. For a 50-bed hospital with a handful of interfaces, that is fine. But when you are running a multi-facility health system with 50+ channels, processing ADT, ORM, ORU, and DFT messages from dozens of source systems, 500 messages per hour means queues backing up, messages timing out, and clinicians waiting for lab results that are stuck in a pipeline.
The good news: the same hardware running default Mirth can be tuned to handle 10,000+ messages per hour with configuration changes alone. No additional servers, no hardware upgrades, no architectural overhaul. Just five specific tuning areas that, applied systematically, deliver a 20x throughput improvement.
This guide covers every tuning parameter with the exact values we use in production, the reasoning behind each change, and the order in which to apply them for maximum impact. This is the guide we wish existed when we were troubleshooting our first Mirth performance crisis at 3 AM.
The Five Tuning Areas (In Order of Impact)
Performance tuning is not random knob-turning. These five areas, applied in this order, deliver cumulative improvements:
- JVM Configuration — The single biggest impact. Default heap is too small for production.
- Message Storage Level — The default stores everything. Production does not need everything.
- Database Optimization — Mirth uses a database for everything. A slow database means slow Mirth.
- Threading and Connection Pooling — Default threading is conservative. Production needs more parallelism.
- Message Pruning and Maintenance — Without pruning, the database grows until it chokes.
1. JVM Configuration: The Foundation
Mirth Connect is a Java application. Its performance ceiling is determined by the JVM configuration. The default installation allocates 512MB of heap memory, which is insufficient for any production workload with more than 10 channels.
The Optimized JVM Configuration
Edit the Mirth startup script (mcservice.vmoptions on Linux, or the Windows service configuration):
# Memory Configuration
-Xms4096m # Initial heap = 4GB
-Xmx4096m # Maximum heap = 4GB (set equal to Xms)
# Garbage Collection
-XX:+UseG1GC # G1 collector (best for heaps over 2GB)
-XX:MaxGCPauseMillis=200 # Target GC pause time
-XX:+ParallelRefProcEnabled # Parallel reference processing
-XX:G1HeapRegionSize=16m # Region size for G1
# Diagnostics
-XX:+HeapDumpOnOutOfMemoryError # Auto-dump on OOM
-XX:HeapDumpPath=/opt/mirth/logs # Dump location
-XX:+PrintGCDetails # GC logging (Java 8)
-XX:+PrintGCDateStamps # GC timestamps
-Xloggc:/opt/mirth/logs/gc.log # GC log file
# Misc
-Djava.awt.headless=true # No GUI needed on server
-Duser.timezone=Asia/Kolkata # Explicit timezone Why Set Xms Equal to Xmx
When Xms (initial heap) is smaller than Xmx (maximum heap), the JVM starts with the smaller allocation and grows the heap as needed. Each growth event causes a GC pause while memory is reallocated. In a message processing pipeline, these pauses cause message processing delays and can trigger connection timeouts. Setting both to the same value allocates all memory upfront, eliminating resize pauses.
Why G1GC Instead of CMS or Parallel GC
For heaps larger than 2GB, G1GC provides the best balance of throughput and latency. It divides the heap into regions and collects the regions with the most garbage first (hence "Garbage First"). This results in shorter, more predictable pause times compared to CMS (Concurrent Mark Sweep), which was the previous default but is deprecated in recent Java versions.
Memory Sizing Guide
Server RAM | Mirth Heap | Database | OS/Other | Channels
-------------|-------------|-------------|------------|----------
8 GB | 2 GB | 4 GB | 2 GB | Up to 20
16 GB | 4 GB | 8 GB | 4 GB | Up to 50
32 GB | 8 GB | 16 GB | 8 GB | Up to 100
64 GB | 16 GB | 32 GB | 16 GB | 100+ Do not allocate more than 16GB to the Mirth JVM even on large servers. Beyond 16GB, GC pauses become longer and the return on additional heap diminishes. If you need more capacity, scale horizontally with Mirth clustering.
2. Message Storage Levels: The Hidden Performance Killer
This is the tuning change that catches every Mirth administrator by surprise. Mirth's message storage level determines how much data is written to the database for every message processed. The default level stores everything, which is useful for development and debugging but devastating for production performance.
The Five Storage Levels
Level | What Gets Stored | Disk I/O | Use Case
--------------|----------------------------------------|-----------|------------------
Development | Raw, Encoded, Sent, Response, Maps | Very High | Dev/test only
Production | Encoded + metadata | Moderate | Production default
Raw | Raw message + metadata | Moderate | Audit requirements
Metadata | Message metadata only (no content) | Low | High-throughput
Disabled | Nothing | None | Maximum throughput
Recommendation:
- Development/test servers: "Development" (see every message detail)
- Production (standard): "Production" (good balance)
- Production (high volume): "Metadata" (10x less I/O)
- Production (max throughput, no audit need): "Disabled" How to Change the Storage Level
In the Mirth Administrator UI: Channel settings > Message Storage tab > Select the storage level. This is a per-channel setting, so you can have development-level storage on a new channel you are debugging while high-volume production channels run at metadata level.
The Impact Is Dramatic
Changing from Development to Production storage typically doubles throughput. Changing from Development to Metadata can deliver a 5-10x improvement because you are eliminating the largest database writes per message. For a channel processing 1,000 messages per hour, the Development level writes approximately 5-10 KB per message to the database. Metadata level writes approximately 200-500 bytes. That is a 20x reduction in database I/O per message.
But What About Troubleshooting?
The concern with reducing storage is losing the ability to reprocess or inspect failed messages. The solution: use an error channel that captures the full message content only for messages that fail. Successful messages at metadata level, failed messages at development level. This gives you the best of both worlds: high throughput for the 99%+ that succeed and full detail for the ones that need investigation.
3. Database Optimization
Mirth uses a database for channel configuration, message storage, and message processing state. The default installation uses Apache Derby, an embedded Java database that is absolutely not suitable for production. Even if you have migrated to PostgreSQL or MySQL (which you must), the default database configuration is not optimized for Mirth's workload pattern.
Step 1: Use PostgreSQL (Not Derby, Not MySQL)
PostgreSQL outperforms MySQL for Mirth's workload pattern (heavy concurrent writes with frequent reads). Derby should never be used in production. Configure Mirth to use an external PostgreSQL instance:
# mirth.properties
database = postgres
database.url = jdbc:postgresql://localhost:5432/mirthdb
database.username = mirth
database.password = [strong-password]
database.max-connections = 50 Step 2: PostgreSQL Configuration
Edit postgresql.conf with these Mirth-optimized values:
# Memory
shared_buffers = 2GB # 25% of total DB server RAM
work_mem = 256MB # Per-operation sort/hash memory
maintenance_work_mem = 512MB # For VACUUM, CREATE INDEX
effective_cache_size = 6GB # 75% of DB server RAM
# Write-Ahead Log
wal_buffers = 64MB # WAL buffer size
checkpoint_completion_target = 0.9 # Spread checkpoint writes
# Connections
max_connections = 100 # Match Mirth pool + admin overhead
max_wal_senders = 0 # Disable if no replication
# Query Planner
random_page_cost = 1.1 # SSD-optimized (default 4.0 is for HDD)
effective_io_concurrency = 200 # SSD-optimized
# Logging
log_min_duration_statement = 1000 # Log queries over 1 second Step 3: Create Indexes
Mirth creates tables for each channel (d_m1, d_m2, etc.) and these tables grow rapidly. Add indexes for common query patterns:
-- Run for each channel table (d_m1, d_m2, ... d_mN)
-- Replace N with your channel number
CREATE INDEX IF NOT EXISTS idx_d_mN_received_date ON d_mN(received_date);
CREATE INDEX IF NOT EXISTS idx_d_mN_status ON d_mN(status);
CREATE INDEX IF NOT EXISTS idx_d_mN_connector ON d_mN(connector_name);
-- For message search performance
CREATE INDEX IF NOT EXISTS idx_d_mm_message_id ON d_mmN(message_id);
CREATE INDEX IF NOT EXISTS idx_d_mc_message_id ON d_mcN(message_id); Step 4: Automated Maintenance
# Add to crontab - run nightly at 2 AM
0 2 * * * psql -U mirth -d mirthdb -c "VACUUM ANALYZE;"
# Weekly full vacuum (reclaims disk space)
0 3 * * 0 psql -U mirth -d mirthdb -c "VACUUM FULL d_m1; VACUUM FULL d_m2;" 4. Threading and Connection Pooling
Mirth's default threading model is conservative: one processing thread per channel, limited connection pool. For high-throughput scenarios, you need more parallelism.
Source Connector Threading
# In channel Source settings:
Source Queue: ON
Source Queue Thread Count: 1-5
# Rules of thumb:
# - TCP/MLLP sources: 1-2 threads (connections are serialized)
# - HTTP sources: 5-10 threads (parallel requests)
# - Database Reader: 1 thread (avoid duplicate reads)
# - File Reader: 1-2 threads Destination Queue Threading
# In channel Destination settings:
Destination Queue: ON
Queue Thread Count: 1-10
# For TCP/MLLP destinations:
# - Single connection to target: 1 thread
# - Target supports multiple connections: 2-5 threads
# For HTTP destinations:
# - 5-10 threads (HTTP is inherently parallel)
# For Database Writer:
# - 2-5 threads (batch inserts benefit from parallelism)
# CRITICAL: More threads is NOT always better.
# Each thread holds a connection to the destination.
# Too many threads overwhelm slow destinations.
# Start with 2 threads, increase while monitoring. Connection Pool Settings
# mirth.properties
database.max-connections = 50 # Default is 20, increase for many channels
# Each channel uses approximately:
# 1 connection for source
# 1 connection per destination
# 1 connection for response handling
# Formula: max-connections >= (number of channels * 3) + 10 5. Message Pruning: Prevent Database Bloat
Without message pruning, Mirth's database grows indefinitely. A channel processing 1,000 messages per hour at Production storage level adds approximately 100 MB per day to the database. After a year, that is 36 GB for a single channel. Multiply by 50 channels and you have a 1.8 TB database that takes hours to query and backs up slowly.
Pruning Configuration
# In Mirth Settings > Message Pruning:
Enable pruning: Yes
Prune completed messages older than: 30 days # Adjust per compliance needs
Prune errored messages older than: 90 days # Keep errors longer for investigation
Prune interval: Daily at 01:00 AM # Off-peak hours
# Per-channel override (for high-volume channels):
# Channel Settings > Message Pruning
# High-volume channels: prune after 7 days
# Low-volume channels: prune after 90 days
# Archive before pruning (if compliance requires):
Archive enabled: Yes
Archive folder: /opt/mirth/archive/ Manual Pruning for Emergency Cleanup
If the database has already grown too large:
-- Check table sizes
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;
-- Emergency prune: delete messages older than 7 days for a specific channel
DELETE FROM d_m1 WHERE received_date < NOW() - INTERVAL '7 days';
DELETE FROM d_mm1 WHERE id NOT IN (SELECT id FROM d_m1);
DELETE FROM d_mc1 WHERE message_id NOT IN (SELECT id FROM d_m1);
VACUUM ANALYZE d_m1;
-- WARNING: For large tables, delete in batches to avoid locking:
DELETE FROM d_m1 WHERE id IN (
SELECT id FROM d_m1 WHERE received_date < NOW() - INTERVAL '7 days' LIMIT 10000
); Advanced Tuning: Beyond the Basics
Channel Processing Order
If you have channels that feed other channels (a common pattern for routing), ensure the source channel has higher priority than the destination channels. In Mirth, processing order within a channel group can be controlled by ordering channels in the group list.
Code Template Optimization
Shared code templates are loaded once per channel deployment, not per message. But poorly written code templates can still cause performance issues:
- Avoid creating new objects (especially Maps and Lists) inside frequently-called functions. Reuse objects where possible
- Use
varinstead ofletin Rhino JavaScript (Mirth's JS engine).lethas block scoping overhead in Rhino - Cache database lookups in channel maps rather than querying per message. Load lookup tables at channel deploy time
- Avoid
logger.info()for every message in production. Uselogger.debug()and set the log level to WARN or ERROR
Monitoring for Performance
Set up Mirth monitoring to track these performance metrics:
Key Metrics to Monitor:
- Messages processed per minute (per channel)
- Queue depth (messages waiting in destination queue)
- Average processing time per message
- Error rate (percentage of messages that fail)
- JVM heap usage (% of allocated heap in use)
- Database connection pool utilization
- Disk I/O (especially for database volume)
Alert Thresholds:
- Queue depth > 100: Warning (processing falling behind)
- Queue depth > 1000: Critical (backlog building)
- Processing time > 5 seconds: Warning (slow transformer or destination)
- Heap usage > 85%: Warning (OOM risk)
- Error rate > 5%: Critical (systematic problem) Performance Anti-Patterns: What NOT to Do
Anti-Pattern 1: Synchronous HTTP Calls in Transformers
Making HTTP calls to external APIs inside a transformer blocks the processing thread until the API responds. If the API takes 2 seconds, your maximum throughput drops to 1,800 messages per hour per thread. Move HTTP calls to a separate destination connector where they can be queued and retried independently.
Anti-Pattern 2: Complex Regex in High-Volume Channels
Regular expressions with backtracking (nested quantifiers, alternation with overlapping patterns) can cause catastrophic performance degradation. A single regex match can take seconds on certain inputs. Use simple string operations (indexOf, substring, split) instead of regex where possible.
Anti-Pattern 3: Logging Every Message
Calling logger.info() with the full message content for every processed message writes to both the Mirth log file and the database. At 10,000 messages per hour, this generates gigabytes of log data per day. Use logger.debug() and only enable debug logging when actively troubleshooting.
Anti-Pattern 4: Not Using Destination Queuing
Without destination queuing, a slow or unavailable destination blocks the entire channel. Enable queuing on every production destination. This decouples source processing from destination delivery, allows retry without reprocessing, and prevents one slow destination from affecting other destinations on the same channel.
Anti-Pattern 5: Running All Channels in One Group
Channel groups are not just organizational. They affect deployment and processing. Deploying a change to one channel in a large group can briefly affect all channels in the group. Group channels by criticality: life-safety interfaces in one group, financial in another, administrative in a third. This also allows different tuning profiles per group.
From architecture to production, our Healthcare Software Product Development team builds healthcare platforms that perform at scale. We also offer specialized Healthcare Interoperability Solutions services. Talk to our team to get started.
Frequently Asked QuestionsHow do I know if my Mirth instance needs tuning?
Check three things: (1) Are destination queues growing over time? If yes, processing is slower than message arrival rate. (2) Is the Mirth dashboard showing high processing times (>1 second per message)? (3) Are clinicians complaining about delayed results or missing data? Any yes answer means tuning is needed. See our guide on common Mirth integration failures for more diagnostic approaches.
Can I tune Mirth without restarting it?
Some changes require restart (JVM settings, database configuration), others do not (message storage level, queue settings, transformer code). Changes that require restart: anything in mcservice.vmoptions or mirth.properties. Changes that do not require restart: channel configuration, code templates, alerts. Plan restarts during maintenance windows.
What is the maximum throughput Mirth can handle?
On a properly tuned 8-core, 32GB server with SSD storage, a single Mirth instance can process 20,000-50,000 messages per hour depending on transformation complexity. For higher volumes, use Mirth clustering or consider an architecture with Kafka for message buffering.
Should I use the embedded Derby database in production?
Absolutely not. Derby is an embedded database intended for development and testing only. It lacks concurrent write performance, proper backup capabilities, and monitoring tools. Migrate to PostgreSQL before going to production. The migration is straightforward: Mirth provides database migration scripts in the installation directory.
How do I measure the impact of each tuning change?
Before making any change, establish a baseline: process a known volume of test messages and record messages per minute, average processing time, and queue depth. Make one change at a time. Re-run the same test. Compare. This disciplined approach tells you exactly which change had the most impact for your specific workload.
The Bottom Line
Mirth Connect performance tuning is not about guessing. It is about systematically addressing five specific bottlenecks in order of impact: JVM heap, message storage level, database configuration, threading, and pruning. Each change is measurable, reversible, and well-understood.
The default Mirth installation is configured for safety and compatibility, not performance. That is appropriate for getting started, but it means every production deployment needs deliberate tuning. The 20x improvement from 500 to 10,000+ messages per hour is achievable on the same hardware with configuration changes alone.
Start with the JVM. Increase the heap. Change the garbage collector. Then adjust storage levels on your high-volume channels. Then optimize the database. The compounding effect of these changes transforms Mirth from a development tool into a production-grade integration engine that handles enterprise healthcare message volumes without breaking a sweat.

