DEV Community

Dhiraj Chatpar
Dhiraj Chatpar

Posted on

exim.conf

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();
};
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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)