Infrastructure · MTA Configuration
How to Configure KumoMTA: init.lua, Listeners, Egress Sources, and Shaping
KumoMTA is configured as code through Lua, not through static text files — all configuration lives in /opt/kumomta/etc/policy/init.lua, which the server loads on startup. You build it up by defining a spool for message storage, starting an ESMTP listener with a relay_hosts list that controls who may inject mail, and an HTTP listener for the injection API. You then define egress sources (each a name, source IP, and EHLO domain) grouped into egress pools that messages are routed through, configure queue management to map tenants to pools with per-domain retry rules, set up DKIM signing, and apply traffic shaping per destination. For people used to static config, KumoMTA ships policy helpers that read TOML files like sources.toml, queues.toml, and shaping.toml so you get the structure without writing all the Lua yourself.
Key takeaways
- Configuration is code. Everything runs through Lua in init.lua, not static config files.
- Listeners gate injection. relay_hosts on the ESMTP listener decides who can send mail through you.
- Sources route, sources shape. Egress sources carry the IP; pools route; shaping happens per source.
- Policy helpers ease the curve. TOML files (sources, queues, shaping) give static-style config atop Lua.
- Shaping can be automated. The TSA daemon reacts to provider responses and throttles in real time.
KumoMTA is an open-source, high-performance MTA built in Rust with a Lua policy layer, designed for high-volume sending. Its defining trait — and the thing that surprises people coming from PowerMTA or Postfix — is that you configure it as code rather than by editing static config files. That’s powerful but unfamiliar, so this guide walks through the configuration in the order it actually builds: understanding the message flow, then init.lua, listeners, egress sources, queues, DKIM, and traffic shaping, using the policy helpers that make it approachable.
Why is KumoMTA configured as code?
The first thing to understand is the philosophy, because it shapes everything else. KumoMTA treats all configuration as Lua policy rather than static text files, and the server loads its configuration from /opt/kumomta/etc/policy/init.lua on startup. This configuration-as-code approach buys real advantages: late loading of config for lower memory use, minimal reloads, and — most importantly — direct connectivity to data sources, so you can do things like pull DKIM signing keys from HashiCorp Vault in real time or check SMTP authentication against a live database, rather than regenerating static files and issuing reload commands.
The honest tradeoff is complexity: configuration-as-code is more flexible but harder to approach than a familiar config file. KumoMTA’s answer is policy helpers — premade Lua scripts that read formatted TOML and JSON files to implement common cases. So in practice you can configure most of a normal deployment by editing TOML files like sources.toml, queues.toml, and shaping.toml, with the helpers translating them into policy, and only drop down to raw Lua when you need deeper integration. That’s the path this guide follows, since it’s how most deployments are actually built.
How does a message flow through KumoMTA?
Before touching config, it pays to understand how a message moves through the server, because the configuration files map directly onto the stages of that flow. The diagram traces the path from injection to delivery.
The key insight from this flow is where each config file acts. A message enters through a listener, lands in a Scheduled Queue keyed by its campaign, tenant, and destination domain, gets moved to a Ready Queue by the queue maintainer (immediately on first attempt, after a retry interval otherwise), and leaves through an egress source toward the destination’s MX. A 4xx temporary failure sends it back to the Scheduled Queue to await retry. Each stage corresponds to a configuration concern: listeners and listener_domains govern entry, queues.toml governs scheduling, and sources plus shaping govern egress.
Building init.lua and the listeners
Configuration begins in init.lua, which opens by requiring the kumo module and then defines the essentials inside an init event. The foundational piece is the spool — where messages are stored on disk — defined with kumo.define_spool pointing at a path like /var/spool/kumomta/data using the RocksDB backend. Alongside it you set up logging and your listeners. One operational note worth knowing: the Lua policy is cached and refreshed every 300 seconds or 1024 executions by default, so policy changes don’t take effect instantly.
The listeners are how mail gets in, and securing them is critical. The ESMTP listener, started with kumo.start_esmtp_listener, carries a relay_hosts setting that controls which networks are authorised to inject mail — by default only localhost and private networks can relay, so you add your injectors’ IPs or CIDR blocks here. Leaving this too open turns your server into an open relay. The HTTP listener, kumo.start_http_listener, exposes the injection API and uses a trusted_hosts list; if you bind it to anything other than localhost, you must also configure TLS and HTTP validation. The terminal later shows the shape of these.
How do you configure inbound and relay domains?
A piece of configuration that’s easy to overlook governs which domains your server will accept mail for and what it does with that mail — controlled by the listener_domains.toml file through the get_listener_domain helper. This is distinct from relay_hosts, which controls who may inject: listener_domains controls which destination domains the server accepts from the outside world, and crucially what kind of traffic it accepts for each. The three things it can permit per domain are inbound relay, the processing of out-of-band bounce notifications, and the processing of feedback loop messages.
The common pattern is a dedicated bounce-and-FBL domain. You configure something like bounce.example.com with log_oob set to true and log_arf set to true but relay_to set to false, which tells the server to accept and log out-of-band DSN bounces and ARF feedback loop reports addressed to that domain, while refusing to act as a general inbound relay for it. This is exactly what a sending operation needs — a clean channel to ingest bounces and complaints — without accidentally turning the server into an inbound mail host. Getting this right is what lets your bounce processing and feedback loops actually receive their data.
Egress sources, pools, and queue management
The egress configuration is where KumoMTA’s model differs most from simpler MTAs, and understanding two distinctions makes it click. An egress source is a configured pathway defined by a name, a source IP, and an EHLO domain; sources are grouped into egress pools, and a message is assigned to a pool through its queue configuration. The distinction that trips people up is that routing happens at the pool level, but traffic shaping happens at the source level — so you choose which pool (and thus which set of IPs) a message uses via queue config, while throttling rules attach to individual sources.
Queue management ties tenants to pools. Using the queues.toml helper, you map a tenant identifier — typically read from an X-Tenant header and then removed for security — to an egress pool, and you set per-domain rules like maximum message age and retry intervals. A related concept is the site_name, an identifier built by merging the MX hostnames of a destination, which lets KumoMTA throttle on the aggregate of all domains sharing the same mail servers rather than per individual domain — so configuring shaping for gmail.com automatically covers every domain behind Google’s MXes. The table summarises the main configuration files and what each governs.
| File | Governs | Key settings |
|---|---|---|
| init.lua | Core policy, spool, listeners | define_spool, start_esmtp_listener |
| sources.toml | Egress sources and pools | source IP, EHLO, pool membership |
| queues.toml | Tenant and queue config | egress_pool, max_age, retry_interval |
| shaping.toml | Per-domain throttling | connection_limit, max_connection_rate |
| listener_domains.toml | Relay, bounces, FBLs | log_oob, log_arf, relay_to |
Using the policy helpers
The policy helpers deserve a closer look, because they’re what make KumoMTA approachable without sacrificing its flexibility. Each helper is a premade Lua script that implements a common configuration concern by reading a formatted TOML or JSON file: the Sources helper configures egress sources and pools from sources.toml, the Queues helper handles tenant and queue configuration, the Shaping helper manages traffic shaping rules, and the Listener_Domains helper handles relay, bounce, and abuse-report domains. You wire each into init.lua with a short setup call, and from then on you mostly edit the TOML files rather than the Lua.
What makes the helpers genuinely valuable beyond convenience is their built-in validation. When you validate a configuration, the helpers cross-check it for consistency — confirming that every pool referenced in your queues actually exists in your sources, that pool membership is valid, and even creating and signing a dummy message for each DKIM domain to catch signing errors before they hit production. This catches the class of mistake that’s otherwise painful to debug: a typo in a pool name or a tenant pointing at a pool that was never defined. The helpers’ source code is also published, so when you outgrow the static TOML approach you can use it as a starting point for a custom integration that pulls configuration directly from your own data sources, which is the natural progression as a deployment matures.
Configuring DKIM signing
Authenticated mail starts with DKIM, and KumoMTA signs messages as they’re received. The straightforward path uses the dkim_signer helper reading a dkim_data.toml file, though you can also sign directly in Lua within the smtp_server_message_received event. The keys themselves you generate conventionally with openssl — creating a private key and extracting the public key to publish in DNS — and store under a structured path like /opt/kumomta/etc/dkim/yourdomain/, owned by the kumod user.
Where KumoMTA’s model pays off is the option to skip static key files entirely. Because configuration is code with live data-source access, you can store DKIM signing keys in HashiCorp Vault and have KumoMTA fetch them for real-time signing, which is genuinely useful at scale where you’re managing keys for many sending domains and want them centralised and rotatable rather than scattered across disk. Either way, DKIM is one leg of the authentication stack that has to be right, alongside SPF and DMARC, which our SPF, DKIM, and DMARC guide covers end to end.
How do you configure traffic shaping?
Traffic shaping is what keeps you within each provider’s tolerances, and it’s configured per destination. In its static form, you write a shaping.toml with per-domain limits — for gmail.com you might set a connection limit, a maximum number of deliveries per connection, a maximum connection rate like 60 per minute, and require TLS; for yahoo.com you’d set tighter numbers. These limits cap how aggressively a given egress source hits each destination, which is the difference between steady delivery and getting temporarily blocked for sending too fast.
The more capable approach is Traffic Shaping Automation, run by a separate TSA daemon. You configure it in a tsa_init.lua with its own HTTP listener (commonly on port 8008) and wire it into init.lua with a setup_with_automation call that publishes and subscribes to that endpoint. What it adds is reactivity: you write rules that watch for specific bounce patterns — a regex matching a “rate too high” response, say — and take automated actions like suspending sending to that destination for an hour or dropping the connection rate when a threshold is crossed. This lets the server respond to a provider’s pushback in real time rather than waiting for you to notice. Before deploying shaping, the validate-shaping binary checks your config, which matters as much for KumoMTA as warming the IPs you’ll send from, covered in our IP warmup guide.
Starting and testing the server
With the configuration in place, you start and verify. The terminal shows the core listener config and the verification steps.
— init.lua essentials local kumo = require ‘kumo’ kumo.on(‘init’, function() kumo.define_spool { name=‘data’, path=‘/var/spool/kumomta/data’, kind=‘RocksDB’ } kumo.start_esmtp_listener { listen=‘0.0.0.0:25’, relay_hosts = { ‘127.0.0.1’, ‘192.168.1.0/24’ } } — who may inject kumo.start_http_listener { listen=‘127.0.0.1:8000’, trusted_hosts = { ‘127.0.0.1’, ‘::1’ } } end) # Start and enable the service $ sudo systemctl start kumomta && sudo systemctl enable kumomta # Test the SMTP listener $ telnet localhost 25 # EHLO / MAIL FROM / RCPT TO $ /opt/kumomta/sbin/validate-shaping etc/policy/custom-shaping.toml
Starting KumoMTA is a matter of systemctl start and enable; it runs the init.lua you’ve built. To test, you can telnet to port 25 and walk through an EHLO, MAIL FROM, and RCPT TO exchange, or use the kcli inject tool to push a test message, then watch the logs — which flush after about 10 seconds by default. Beyond the mechanics, the deliverability fundamentals still apply: use a consistent HELO/EHLO hostname, have SPF, DKIM, and DMARC properly configured, add List-Unsubscribe headers, and monitor Postmaster Tools. KumoMTA gives you a fast, deeply configurable engine, but it’s still one component of a healthy sending operation, as our deliverability guide lays out — and for teams that want that engine on dedicated, controllable hardware, our email infrastructure hosting provides the foundation it runs on.