PowerMTA Lua Scripting: 5 Practical Examples for Routing & Retries
Get hands-on PowerMTA Lua scripting examples that show how to route by domain, override retry logic based on SMTP codes, and prioritise high-value queues. Includes testing tips and a managed-SMTP comparison.

If you run PowerMTA at any real volume, the stock configuration only gets you so far. To control which IP pool carries which recipient domain, how many times a deferred message retries, and which queue gets priority, you need the Lua scripting layer. This article collects practical powermta lua scripting examples you can copy into your own environment, covering routing, retry logic, and queue policies.
By the end, you will be able to write Lua hooks that select a VirtualMTA based on recipient domain, override PowerMTA's default retry behaviour when specific SMTP error codes appear, and raise queue priority for high-value domains. You will also see how to test and debug Lua scripts before rolling them into production.
How Lua Hooks Work in PowerMTA
PowerMTA embeds a Lua interpreter and lets you register a Lua file in the main configuration with a directive such as lua-file /etc/pmta/scripts/routing.lua. Once registered, PowerMTA calls functions inside that file at fixed points in the delivery lifecycle. The most common points are when a connection is opened, when a message is accepted, when individual recipients are processed, and after a delivery attempt completes.
# /etc/pmta/pmta.conf
lua-file /etc/pmta/scripts/routing.lua
The table below shows the typical hook points and what you can influence at each stage.
| Hook point | Typical function name | What you can change |
|---|---|---|
| Connection | pmta.on_connect() | VirtualMTA, source IP, rate limits |
| Message | pmta.on_message() | Queue priority, message metadata |
| Recipient | pmta.on_recipient() | Per-recipient VMTA, retry flags |
| Delivery | pmta.on_delivery() | Retry, bounce, log decisions |
The examples below use the common pmta.get_recipient() and pmta.get_message() accessors, plus pmta.log() for debugging. Always consult the Lua API reference for your exact PowerMTA version before relying on a property name.
Example 1: Route by Recipient Domain or IP Pool
A common requirement is to send Gmail-bound mail from one set of IPs and Outlook-bound mail from another. This keeps each pool's reputation isolated and lets you warm up IPs independently. The following pmta.on_recipient() hook inspects the recipient domain and sets the VirtualMTA on the connection accordingly. The VirtualMTA names must match those defined in your pmta.conf.
function pmta.on_recipient()
local rcpt = pmta.get_recipient()
local conn = pmta.get_connection()
local domain = rcpt.domain:lower()
if domain == "gmail.com" then
conn:set("vmta", "gmail-pool")
elseif domain == "outlook.com" or domain == "hotmail.com" then
conn:set("vmta", "outlook-pool")
else
conn:set("vmta", "default-pool")
end
end
After reloading PowerMTA, send a test message to a Gmail address and confirm in the delivery logs that the gmail-pool VirtualMTA was used. If the domain does not match, the default pool applies. You can also add a pmta.log("routing to gmail-pool for " .. domain) line to trace the decision in real time.
Example 2: Custom Retry Logic Based on SMTP Error Codes
PowerMTA already retries deferred messages, but its default policy may not match your needs. For example, you may want to stop retrying after a 550 permanent failure, or back off more aggressively after a 421 service-not-available deferral. The following hook runs after each delivery attempt and inspects the SMTP status code.
function pmta.on_delivery()
local rcpt = pmta.get_recipient()
local status = rcpt:get("delivery_status")
local code = tonumber(rcpt:get("smtp_code") or "0")
if status == "failed" and code == 550 then
rcpt:set("retry", "false")
rcpt:set("bounce", "true")
elseif status == "deferred" and code == 421 then
rcpt:set("retry", "true")
rcpt:set("retry_delay", "1800")
end
end
A 550 code usually means the mailbox does not exist, so retrying wastes resources and harms your sending reputation. A 421 code often signals a temporary problem at the receiving server; forcing a 30-minute delay instead of PowerMTA's default can reduce the chance of repeated deferrals. Always test with a controlled domain before applying this globally, because some providers use 421 for rate limiting and a shorter delay may be more appropriate.
Example 3: Dynamic Queue Priority for High-Value Domains
If you send large batches, PowerMTA's queue can become a bottleneck. You can use Lua to ensure that mail to your most engaged or highest-revenue domains leaves before bulk traffic. The pmta.on_message() hook below assigns a higher queue priority for specific domains.
function pmta.on_message()
local msg = pmta.get_message()
local rcpt = pmta.get_recipient()
local domain = rcpt.domain:lower()
if domain == "gmail.com" or domain == "yahoo.com" then
msg:set("queue_priority", "high")
else
msg:set("queue_priority", "normal")
end
end
The queue_priority value maps to the queue-priority settings in your pmta.conf. A high priority queue is processed before the normal queue. Be careful not to mark too many messages as high priority, otherwise the distinction becomes meaningless.
Testing and Debugging Lua Scripts in PowerMTA
Before deploying a Lua change, validate the configuration syntax and check the logs. Use pmta --test-config to catch syntax errors, and then reload PowerMTA with pmta reload. If your script has a runtime error, PowerMTA logs it and continues with the default behaviour, so you may need to raise the log level.
pmta --test-config
pmta reload
tail -f /var/log/pmta/log
Follow this sequence every time you edit a Lua file:
- Write the Lua file and place it in a known path such as
/etc/pmta/scripts/routing.lua. - Add the
lua-filedirective topmta.conf. - Run
pmta --test-configto catch syntax errors. - Reload and send a test message to a controlled domain.
- Watch the log for
Lua errorentries or unexpected behaviour.
If you manage several PowerMTA servers, repeating these Lua changes by hand across each host is tedious. PMTAcore is a Windows desktop application that automates PowerMTA installation over SSH and centralises configuration tasks such as DNS record generation and IP rotation. The Lua scripts in this article still apply once PowerMTA is installed; PMTAcore can help you manage the infrastructure around them. See the PowerMTA Management page for details.
When you combine custom retry logic with multi-server campaigns, PMTAcore's Campaign Manager can enforce per-server rate limits while your Lua hooks decide retry behaviour. For bulk senders who also need a self-hosted marketing platform with open and click tracking, Choco Mailer is a separate paid addon.
Monitoring your sending IPs for blacklist entries is essential when you change retry or routing policies, because a sudden spike in retries can hurt reputation. PMTAcore's IP Blacklist Checker streams DNSBL results for single IPs or whole server groups.
Managed SMTP vs PowerMTA Lua: A Quick Comparison
If you are evaluating this level of control against a managed SMTP API, the common questions still apply. Is Mailgun better than SendGrid? In independent 2026 tests, Mailgun tends to offer more flexible SMTP and better log visibility, while SendGrid wins on SDK breadth and marketing features. What is better than SendGrid? For developers who need per-recipient routing and retry overrides, PowerMTA with Lua scripting provides control no cloud API exposes. Which transactional email service is the best? There is no single best; it depends on volume, deliverability requirements, and whether you want to manage infrastructure. PowerMTA is a self-hosted MTA, so you own the IPs and the queue, which is exactly where Lua scripting matters.
Start with the free trial. For full licence options, see the pricing page.
Related Articles

Advanced PowerMTA Configuration 2026: Tuning for Deliverability
Master PowerMTA performance tuning and config best practices for 2026. Learn key parameters, virtual MTA and IP rotation, queue management, retry strategies, and advanced deliverability settings to maximize inbox placement.
Read more →
PowerMTA vs MailerQ: Which MTA Should You Use in 2026?
A practical comparison of PowerMTA and MailerQ covering architecture, throughput, management and pricing to help high-volume senders choose the right MTA.
Read more →
PowerMTA 5.0 Features and Upgrade Guide (2026)
A practical walkthrough of PowerMTA 5.0's HTTP APIs, redesigned web monitor, and upgrade steps from 4.5, including rollback checks.
Read more →