Stalwart as an MTA Relay

Your question

I’m trying to configure a secondary stalwart instance on another server to act as a backup for when my primary one goes offline, receiving messages and storing them to then forward on to my primary server once it regains connectivity. I have it mostly working however I cannot figure out how to get it to retain messages, as when it receives a message it arrives in the queue and then immediately leaves and responds to the original recipient the following message: (connection to ‘[relay server IP]’ failed: record not found for MX)
How can I get it to hold on to these messages and only deliver them to my primary server when it comes back online?

Stalwart 0.16.11

The root cause is that your backup Stalwart instance is using MX-based routing (the default), so when it can’t resolve an MX record for your primary server’s domain (because it’s looking up DNS rather than routing directly to a known IP), it treats this as a permanent or unrecoverable failure and bounces immediately. There are two things you need to fix:

1. Use a Relay route to your primary server instead of MX resolution

Create an MtaRoute of the Relay variant pointing directly at your primary server’s IP/hostname. This bypasses DNS MX lookup entirely:

{
  "@type": "Relay",
  "name": "primary",
  "address": "<primary-server-IP-or-hostname>",
  "port": 25,
  "protocol": "smtp",
  "implicitTls": false,
  "allowInvalidCerts": false,
  "authSecret": {"@type": "None"}
}

Then in your MtaOutboundStrategy, set the route expression to select "primary" for your local domain(s) (e.g. is_local_domain('', rcpt_domain)). This way the backup will always try to deliver to the primary directly, not via DNS.

2. Configure retry/expiry so messages are held on connection failure

The connection failure needs to be treated as a temporary failure so the queue retries rather than bouncing. A connection failure (like the one you’re seeing) is a temporary error and Stalwart should retry – but the default MtaDeliverySchedule may have a low maxAttempts or short TTL. Create or adjust an MtaDeliverySchedule with longer retry intervals and a longer expiry, for example:

{
  "name": "backup-relay",
  "queueId": "<your-queue-id>",
  "retry": {
    "@type": "Custom",
    "intervals": [
      {"duration": "5m"}, {"duration": "15m"}, {"duration": "30m"},
      {"duration": "1h"}, {"duration": "2h"}, {"duration": "4h"}
    ]
  },
  "notify": {"@type": "Default"},
  "expiry": {"@type": "Ttl", "expire": "3d"}
}

The key point is that with Ttl-based expiry set to 3d, messages will stay in the queue and be retried for up to 3 days – giving your primary server plenty of time to come back online.

These are configured in the WebUI under Settings > MTA > Outbound > Routes and Settings > MTA > Outbound > Delivery Schedules.

Docs: Routing | Scheduling

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

I have done all of that already, it is set to use a Relay route to my primary server IP and has a TTL of 10 days.

I think I fixed it, turns out I had set my MX records incorrectly.