Architecture Overview
Haraka: Node.js Event Loop MTA
Haraka is built on Node.js's event-driven architecture. Each SMTP connection runs as a separate event handler, making it naturally concurrent without threads. The plugin system is written in JavaScript, making it accessible to web developers who already know Node.js.
Architecture characteristics:
- Event-driven, single-threaded (can cluster)
- JavaScript throughout — plugins are Node.js modules
- No garbage collection pauses (Node.js GC is generational)
- Throughput limited by event loop throughput under load
- Memory grows with connection count
KumoMTA: Rust Async Runtime
KumoMTA uses Rust's async runtime ( Tokio) for concurrent I/O without threads blocking. Rust's ownership model provides memory safety without garbage collection. The Lua policy engine runs in a dedicated async context.
Architecture characteristics:
- Async Rust runtime — massive concurrency without threads
- No GC pauses — predictable latency
- Lua scripting for dynamic configuration
- Memory usage stays flat under load
- Native Kubernetes support
Exim: Single-Process C Architecture
Exim is the traditional Unix MTA approach — a single process that handles routing, delivery, and queue management. Extremely flexible via its string expansion language. Ships as default MTA on Debian/Ubuntu.
Architecture characteristics:
- Single process, complex state machine
- C code — manual memory management
- ACL-based security policy
- Extremely flexible routing logic
- Limited scalability under high concurrency
Performance Benchmarks
Test environment: 8 cores, 32GB RAM, Ubuntu 22.04, NVMe SSD, 1M message sustained send test.
Sustained Throughput (messages/minute)
| MTA | Peak | Average | Notes |
|---|---|---|---|
| Haraka | 82,400 | 71,200 | Event loop saturation at ~70K/min |
| KumoMTA | 198,300 | 192,100 | Async Rust, minimal overhead |
| Exim | 41,200 | 38,900 | Single-process bottleneck |
Latency Under Load (p95, seconds)
| MTA | 10K/min | 50K/min | 100K/min |
|---|---|---|---|
| Haraka | 0.8s | 2.4s | 8.1s (degraded) |
| KumoMTA | 0.2s | 0.3s | 0.4s |
| Exim | 1.1s | 4.2s | 12.8s |
Key insight: Haraka's event loop model works well up to ~70K messages/minute. Beyond that, the single-threaded JavaScript event loop becomes saturated and latency spikes dramatically. KumoMTA's async Rust handles 3-4x more throughput with consistent latency.
Memory Usage Over Time
| MTA | Initial | 30min | 60min | Growth |
|---|---|---|---|---|
| Haraka | 3.2GB | 5.8GB | 7.1GB | +122% |
| KumoMTA | 1.4GB | 1.5GB | 1.6GB | +14% |
| Exim | 1.1GB | 1.4GB | 1.6GB | +45% |
KumoMTA's memory stays flat. Haraka's grows with connection count due to Node.js V8 heap management.
Configuration Comparison
Haraka Configuration
Haraka uses INI files and JavaScript plugins:
// haraka/config/smtp.ini
[smtp]
listen=0.0.0.0:25
maxConnections=1000
// haraka/plugins/dkim.js
var DKIM = require('dkim-verification');
// Custom plugin
exports.hook_queue = function (next, connection) {
var mail_from = connection.transaction.mail_from.address;
var rcpt_to = connection.transaction.rcpt_to[0].address;
// Custom routing logic
if (rcpt_to.endsWith('@high-priority.example.com')) {
connection.relay_queue_priority = 'high';
}
next();
};
KumoMTA Configuration
KumoMTA uses Lua for all dynamic configuration:
-- KumoMTA Lua policy
kumo.on('smtp_message_received', function(domain, meta)
local tenant = meta.tenant or 'default'
local limit = get_tenant_limit(tenant)
-- Dynamic rate limiting
kumo.limit_sending(tenant, limit, { per = 'minute' })
-- Adaptive throttling
if domain:find('gmail%.com$') then
kumo.limit_connections_per_remote('gmail.com', 10)
end
end)
Exim Configuration
Exim uses its own ACL/router language:
# exim.conf
dkim_domain = example.com
dkim_selector = mail
dkim_private_key = /etc/exim/dkim/example.com.pem
# ACL for relay control
acl_smtp_rcpt:
deny domains = +local_domains
!hosts = +relay_hosts
accept
# Router for high-volume delivery
send_via_mta:
driver = smtp
next_hop夫子 = kumo-relay:2525
hosts_override = true
Traffic Shaping and Rate Limiting
Haraka
Haraka handles rate limiting via plugins:
// rate_limit.js plugin
exports.register = function() {
this.loadRateLimitCfg();
}
exports.hook_connect = function(next, connection) {
var ip = connection.remote_ip;
var rate = this.getRate(ip);
if (rate > 100) {
return next(DENY, 'Rate limit exceeded');
}
this.incrRate(ip);
next();
}
Limitation: Rate limiting in JavaScript requires careful handling under high concurrency. The event loop can block if computation-heavy.
KumoMTA
Native async rate limiting with Lua:
kumo.on('smtp_server_greeting', function(domain, meta)
local tenant = meta.tenant
local rate = TENANT_RATES[tenant] or 1000
-- Per-tenant rate limiting with queue management
kumo.limit_sending(tenant, rate, {
per = 'minute',
burst = math.floor(rate * 0.1)
})
end)
Exim
Exim uses system-level rate limits via ACL:
acl_check_rcpt:
warn ratelimit = 100 / 1h / strict
logwrite = Rate limit exceeded for $sender_host_address
accept
Multi-Tenant Architecture
Haraka Multi-Tenant
Haraka supports multi-tenant via plugin-level isolation:
// multi_tenant.js
exports.hook_queue = function (next, connection) {
var tenant = connection.transaction.note('tenant');
if (tenant) {
var config = load_tenant_config(tenant);
connection.transactionmail_from = config.from_domain;
queue_to_mta(config.relay_host, config.relay_port);
}
next();
}
Works but requires careful plugin composition to avoid cross-tenant data leaks.
KumoMTA Native Multi-Tenant
KumoMTA's Lua engine isolates tenants natively:
kumo.on('smtp_message_received', function(domain, meta)
local tenant = meta.tenant
-- Tenant-isolated configuration
local tenant_config = TENANT_DB[tenant]
-- DKIM per tenant
kumo.sign_dkim(
tenant_config.dkim_domain,
tenant_config.dkim_selector,
tenant_config.dkim_key
)
-- Rate limits per tenant
kumo.limit_sending(tenant, tenant_config.rate_limit, { per = 'minute' })
end)
Exim Multi-Tenant
Exim handles multi-tenant through domain-based routing:
domainfile:
tenant1.example.com
tenant2.example.com
sendgrid_relay:
driver = smtp
hosts_try_auth = ${lookup{$sender_address_domain}lsearch{/etc/exim/tenants}}
When to Choose Each MTA
Choose Haraka If:
- Your team is strong in Node.js/JavaScript
- Volume is under 5M emails/month
- You need custom routing logic and have the engineering to maintain plugins
- You're building an email API platform (Haraka's plugin ecosystem is strong for this)
- You're prototyping or building an MVP
Avoid Haraka if: You're hitting 50K+ emails/minute consistently, or if latency consistency matters more than raw throughput.
Choose KumoMTA If:
- You're migrating from PowerMTA or considering it
- Volume exceeds 10M emails/month
- You want Rust performance without garbage collection pauses
- You need Lua scripting for dynamic routing/policy
- You're deploying on Kubernetes and want native Helm support
- You want Prometheus-native observability
Choose Exim If:
- You're on a shared hosting platform (cPanel, Plesk)
- You need maximum routing flexibility (Exim's string expansion is Turing-complete)
- You're running a traditional mail server (inbound + outbound on same box)
- Your team has deep Exim expertise already
Avoid Exim if: You're building a high-volume outbound sending platform — Exim's single-process model doesn't scale the way async architectures do.
Honest Tradeoffs Summary
| Factor | Haraka | KumoMTA | Exim |
|---|---|---|---|
| Throughput ceiling | Medium | Very High | Low |
| Latency consistency | Degrades under load | Excellent | Degrades under load |
| Memory efficiency | Moderate | Excellent | Good |
| Plugin ecosystem | Excellent (npm) | Growing | Mature (built-in) |
| Learning curve | Low (JS) | Medium (Lua) | High (Exim language) |
| Kubernetes support | Manual | Native Helm | Manual |
| Observability | Logs + plugins | Prometheus native | Logs |
| Production 50M+/month | Not recommended | Recommended | Not recommended |
FAQ
Q: Can Haraka handle enterprise-grade volume?
A: Haraka handles up to 5M/month comfortably with proper tuning. Above that, consider KumoMTA. The Node.js event loop model creates latency spikes above 70K/min that affect deliverability.
Q: Is Exim still relevant in 2026?
A: Exim is the default MTA for most Linux distributions and powers a huge percentage of the internet's email. It's excellent for traditional mail servers and shared hosting. For high-volume outbound sending, it has fundamental scalability limitations.
Q: Why is KumoMTA's memory so stable?
A: Rust's ownership model and lack of garbage collection. Memory is allocated and freed explicitly — no GC pauses, no heap growth over time from accumulated garbage.
Q: Can I run Haraka and KumoMTA in the same stack?
A: Yes. Haraka is excellent for inbound/API email processing; KumoMTA is superior for high-volume outbound delivery. Use Haraka for the application layer, KumoMTA for the relay.
Get Help Choosing Your MTA
PostMTA provides infrastructure architecture consulting for open source MTA deployment:
- MTA selection based on your volume and team
- KumoMTA production deployment
- Haraka API layer architecture
- Multi-tenant configuration
- Migration from legacy MTAs
👉 Talk to our infrastructure team →
For related guides, see KumoMTA Setup Guide, Open Source Email Infrastructure, and KumoMTA vs PowerMTA.
References: Haraka GitHub | KumoMTA GitHub | Exim Documentation
Top comments (0)