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.

Message flow through KumoMTAInjectionSMTP / HTTP APIScheduled Queuecampaign:tenant:domainReady Queuethrottled by shapingEgress sourcesource IP + EHLODest MX4xx tempfail → returns to Scheduled Queue for retry
Mail is injected, scheduled, made ready under shaping limits, then sent via an egress source; tempfails loop back for retry.

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.

KumoMTA configuration files and their roles.
FileGovernsKey settings
init.luaCore policy, spool, listenersdefine_spool, start_esmtp_listener
sources.tomlEgress sources and poolssource IP, EHLO, pool membership
queues.tomlTenant and queue configegress_pool, max_age, retry_interval
shaping.tomlPer-domain throttlingconnection_limit, max_connection_rate
listener_domains.tomlRelay, bounces, FBLslog_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.

kumomta-init-and-test
— 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.

Frequently asked questions

How do I configure KumoMTA?
KumoMTA is configured as code through Lua, with all configuration loaded from /opt/kumomta/etc/policy/init.lua on startup. You define a spool for message storage, start an ESMTP listener with a relay_hosts list controlling who may inject mail, and an HTTP listener for the injection API. You then define egress sources (name, source IP, EHLO domain) grouped into pools, configure queue management mapping tenants to pools, set up DKIM signing, and apply traffic shaping per destination. For those used to static config, policy helpers read TOML files like sources.toml, queues.toml, and shaping.toml so you get structure without writing all the Lua yourself.
Why does KumoMTA use Lua instead of config files?
KumoMTA treats all configuration as code rather than static text files, which buys several advantages: late loading of config for lower memory use, minimal reloads, and direct connectivity to data sources — so you can pull DKIM keys from HashiCorp Vault for real-time signing or check SMTP authentication against a live database. The tradeoff is complexity, since code is harder to approach than a familiar config file. KumoMTA addresses this with policy helpers: premade Lua scripts that read TOML and JSON files, letting you configure most deployments by editing TOML and only dropping to raw Lua for deeper integration.
What is an egress source in KumoMTA?
An egress source is a configured pathway defined by a name, a source IP address, and an EHLO domain. Sources are grouped into egress pools, and a message is assigned to a pool through its queue configuration. The important distinction is that routing happens at the pool level — you choose which pool, and thus which IPs, a message uses — while traffic shaping happens at the source level, so throttling rules attach to individual sources. This separation lets you route mail by tenant or campaign to a set of IPs while independently controlling how fast each IP sends to each destination.
How does traffic shaping work in KumoMTA?
Traffic shaping is configured per destination domain. In its static form, you write a shaping.toml with per-domain limits like connection limit, deliveries per connection, maximum connection rate, and TLS requirements — tighter for stricter providers. The more capable approach is Traffic Shaping Automation, run by a separate TSA daemon configured in tsa_init.lua and wired into init.lua. It watches for specific bounce patterns via regex and takes automated actions like suspending sending to a destination or lowering the connection rate when thresholds are crossed, letting the server react to a provider’s pushback in real time.
How do I control who can send mail through KumoMTA?
Through the relay_hosts setting on the ESMTP listener. By default, only localhost and private networks are allowed to relay mail, so you add the IP addresses or CIDR blocks of your authorised injectors to that list. Leaving it too open turns your server into an open relay, which gets you blacklisted quickly. The HTTP injection listener uses a separate trusted_hosts list, and if you bind it to any address other than localhost, you must also configure TLS and HTTP validation. Getting these listener controls right is the first security step in any deployment.