# Cron "No MTA Installed, Discarding Output": Meaning and Fixes

By Dan Ochoa · Published 2026-10-25 · Updated 2026-10-25
Canonical: https://cronradar.com/blog/cron-no-mta-installed

> What cron's "No MTA installed, discarding output" message means, why the job still ran, and three fixes: log the output, silence it, or install a relay MTA.

**`(CRON) info (No MTA installed, discarding output)` means a cron job finished, printed something to standard output or standard error, and cron couldn't email it because the machine has no mail transfer agent at `/usr/sbin/sendmail`, so the output was deleted.** The job ran: cron logs the line after the job exits. What's gone is whatever it printed, error messages included. The message comes from Debian's cron package, which Debian and Ubuntu use; cronie and BusyBox crond handle the same situation differently. Versions below: cron 3.0pl1-197 (Debian 13), 3.0pl1-184ubuntu2 (Ubuntu 24.04), cronie 1.7.2, and BusyBox 1.37.0, checked October 24, 2026.

## What Triggers the Message

Debian's cron collects a job's standard output and standard error in one temporary file ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L277)). When the job exits, it checks, in order ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L506)):

1. No output: nothing happens. A silent job never produces this message.
2. `MAILTO` set but empty: the output is discarded without a log line.
3. Otherwise the recipient is `MAILTO`, or the crontab's owner if `MAILTO` isn't set.
4. If `/usr/sbin/sendmail` doesn't exist, cron logs `No MTA installed, discarding output` and discards the output ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L524)).

The path is glibc's `_PATH_SENDMAIL` ([paths.h](https://github.com/bminor/glibc/blob/glibc-2.41/sysdeps/unix/sysv/linux/paths.h#L59)). Cron only checks that the file exists, so any package that installs `/usr/sbin/sendmail` stops the message, whether or not its mail gets delivered. Ubuntu 24.04's cron package only suggests an MTA and Debian 13's only recommends one ([Ubuntu](https://git.launchpad.net/ubuntu/+source/cron/tree/debian/control?h=applied/ubuntu/noble), [Debian](https://sources.debian.org/src/cron/3.0pl1-197/debian/control/)). apt skips suggested packages unless told otherwise, and `--no-install-recommends` skips recommended ones ([apt-get(8)](https://manpages.debian.org/trixie/apt/apt-get.8.en.html)).

### Find the Job That Printed It

The message doesn't name the job, and the line can come from any crontab, including `/etc/crontab` and `/etc/cron.d`; the [crontab guide](https://cronradar.com/blog/crontab-guide) shows how to list them all. By default, the `CMD` line comes from the job's process and the MTA line from its parent, so their PIDs differ. Starting cron with `-L 15` logs each job's start, end, non-zero exit status, and PID, all under the same PID as the MTA line ([cron(8)](https://manpages.debian.org/trixie/cron/cron.8.en.html)). On Debian 13, set `EXTRA_OPTS='-L 15'` in `/etc/default/cron`. On Ubuntu 24.04, where that file is deprecated, run `systemctl edit cron` and add:

```ini
[Service]
Environment="EXTRA_OPTS=-L 15"
```

After `systemctl restart cron`, `journalctl -u cron` groups each run:

```text
CRON[48211]: (deploy) CMD ([48213] /srv/app/bin/nightly-report)
CRON[48211]: (CRON) error (grandchild #48213 failed with exit status 1)
CRON[48211]: (CRON) info (No MTA installed, discarding output)
CRON[48211]: (deploy) END ([48213] /srv/app/bin/nightly-report)
```

## How Cron Mail Works in Each Implementation

|                   | Debian cron 3.0pl1                                 | cronie 1.7.2                                  | BusyBox crond 1.37.0                                             |
| ----------------- | -------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------- |
| Mails when        | The job printed anything                           | The job printed anything                      | The job printed anything and the crontab sets `MAILTO`           |
| Default recipient | Crontab owner                                      | Crontab owner                                 | None                                                             |
| `MAILFROM`        | Sets the From header; default `root (Cron Daemon)` | Sets the sender; default the owner's username | Not supported                                                    |
| Mailer            | `/usr/sbin/sendmail`, run as the owner             | `/usr/sbin/sendmail` or the `-m` command      | `sendmail -ti` from crond's `PATH`                               |
| No mailer         | Logs `No MTA installed, discarding output`         | Writes the output to syslog                   | Logs `can't execute 'sendmail' for user ...`; the output is lost |
| Turn mail off     | `MAILTO=""`                                        | `MAILTO=""` or `crond -m off`                 | Leave `MAILTO` unset                                             |

cronie decides at startup. If crond was started without `-s` or `-m` and `/usr/sbin/sendmail` isn't executable, it logs `Syslog will be used instead of sendmail.` ([cron.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/cron.c#L305-L308)) and writes each output line to syslog as `(user) CMDOUT (line)` ([do_command.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/do_command.c#L553-L568)). RHEL 9's cronie 1.5.7 does the same. Because the check runs once, restart crond after installing an MTA. cronie 1.7.1 and later also accept `-n` before a command, as in `15 3 * * * -n /srv/app/bin/nightly-report`, to mail the output only when the job exits non-zero ([NEWS](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/NEWS)).

BusyBox crond captures output only for entries under a `MAILTO=` line ([crond.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/miscutils/crond.c#L765)). Everything else goes to crond's own stdout and stderr: `/dev/null` when crond runs as a daemon, its default ([vfork_daemon_rexec.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/libbb/vfork_daemon_rexec.c#L318-L321)), or the terminal or container log with `crond -f`. Alpine builds in BusyBox's own `sendmail` applet ([busyboxconfig](https://gitlab.alpinelinux.org/alpine/aports/-/blob/3.22-stable/main/busybox/busyboxconfig)), which sends to the SMTP server named in `SMTPHOST` in crond's environment, or to 127.0.0.1 ([sendmail.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/mailutils/sendmail.c#L256)).

Debian cron and cronie both add an `X-Cron-Env` header for every variable in the job's environment, crontab variables included ([Debian](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L597), [cronie](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/do_command.c#L539-L540)). A password or API key set as a crontab variable goes out in the headers of every cron email.

## Fix 1: Send the Output to a File or the Journal

This keeps what the job printed. Pick one per entry:

```text
# Append to a file in a directory the crontab's owner can write
15 3 * * * /srv/app/bin/nightly-report >> /var/log/app/nightly-report.log 2>&1

# Send stdout and stderr to the journal; the entry keeps the job's exit status
15 3 * * * systemd-cat -t nightly-report /srv/app/bin/nightly-report

# Pipe to logger; with bash, pipefail keeps the job's failure status
SHELL=/bin/bash
15 3 * * * set -o pipefail; /srv/app/bin/nightly-report 2>&1 | logger -t nightly-report
```

Read the last two with `journalctl -t nightly-report`. systemd-cat connects the command's stdout and stderr to the journal and then replaces itself with the command ([systemd-cat(1)](https://www.freedesktop.org/software/systemd/man/latest/systemd-cat.html), [cat.c](https://github.com/systemd/systemd/blob/v257/src/journal/cat.c#L177)), so cron sees the job's own status. Add `--stderr-priority=err` to log stderr lines as errors.

A pipeline's exit status is its last command's, so without `pipefail` a failed job piped to `logger` reports success to anything that checks, such as a monitoring wrapper or `-L 4` logging. Support for `set -o pipefail` in `/bin/sh` varies: bash and Alpine's BusyBox ash have it, Debian 13's dash gained it in 0.5.12-7 ([changelog](https://sources.debian.org/src/dash/0.5.12-12/debian/changelog/)), and Ubuntu 24.04's dash 0.5.12-6ubuntu5 lacks it. That's why the example sets `SHELL=/bin/bash`, which applies to every entry below it.

A log file grows until something rotates it, such as a logrotate rule. [How to know if a cron job ran](https://cronradar.com/blog/how-to-know-if-cron-job-ran) shows how to log each run's exit code beside its output.

## Fix 2: Silence It on Purpose

`MAILTO=""` at the top of a crontab turns mail off for every entry below it. On Debian and Ubuntu that also stops the MTA message, because the empty check comes first. On cronie with no MTA, output still goes to syslog, since that copy doesn't depend on `MAILTO`. [Cron environment variables](https://cronradar.com/blog/cron-environment-variables) covers how crontab variables are parsed, including BusyBox's narrower rules.

`> /dev/null 2>&1` on an entry discards its output before cron sees it. Either way, the error messages go with the noise, and the next failure leaves nothing to read in mail, the journal, or on disk. If a job is noisy only when it succeeds, discard stdout and keep stderr:

```text
15 3 * * * /srv/app/bin/nightly-report > /dev/null 2>> /var/log/app/nightly-report.err
```

## Fix 3: Install an MTA That Relays

A cron host needs to send mail, not receive it. Debian 13 and Ubuntu 24.04 both package `msmtp-mta`, `postfix`, `dma`, and `nullmailer`, each of which provides `/usr/sbin/sendmail`. The two most common choices:

- **msmtp-mta** (1.8.28 on Debian 13, 1.8.24 on Ubuntu 24.04) is an SMTP client that hands each message to your relay as cron passes it over ([msmtp manual](https://marlam.de/msmtp/msmtp.html)). It has no queue of its own.
- **Postfix** queues mail and retries. Choose **Satellite system** during installation, which sends all mail to a smarthost ([debconf templates](https://sources.debian.org/src/postfix/3.10.13-0+deb13u1/debian/templates/)); Postfix's [null client example](https://www.postfix.org/STANDARD_CONFIGURATION_README.html#null_client) also disables listening on the network and local delivery. Relay credentials go in `/etc/postfix/sasl_passwd`, readable only by root ([SASL_README](https://www.postfix.org/SASL_README.html#client_sasl_enable)).

For msmtp, run `apt install msmtp-mta`, then create `/etc/msmtprc`:

```text
defaults
tls on
syslog LOG_MAIL
aliases /etc/aliases

account default
host smtp.example.com
port 587
from cron@example.com
set_from_header on
# If the relay requires a login, also set:
# auth on
# user cron@example.com
# password followed by the relay account's password
```

and `/etc/aliases`, which maps local names such as `root` or `deploy`, the recipient when `MAILTO` is unset, to real addresses:

```text
default: ops@example.com
```

`set_from_header on` replaces cron's From header with the `from` address. Debian's default, `root (Cron Daemon)`, has no domain, and msmtp's manual calls rejecting a From header that doesn't match the sender a common anti-spam measure.

Mind the file's permissions. Debian's msmtp stopped being setgid in 1.8.22 ([NEWS](https://sources.debian.org/src/msmtp/1.8.28-3/debian/NEWS/)), and Debian's cron runs the mailer as the crontab's owner ([popen.c](https://sources.debian.org/src/cron/3.0pl1-197/popen.c/#L141)), as cronie does. So `/etc/msmtprc` must be readable by every account whose cron mail it sends, and so is any password in it. If only root's jobs send mail, `chmod 600 /etc/msmtprc`; otherwise, use a relay that accepts the server without a login, or Postfix.

Test the mailer directly, then through cron:

```bash
printf 'Subject: cron mail test\n\nSent from %s.\n' "$(hostname)" | /usr/sbin/sendmail ops@example.com
echo "sendmail exit status: $?"
```

msmtp's exit status says whether the relay accepted the message. Postfix's `sendmail` only places it in the queue, so `mailq` is where to look for anything still waiting. Then add `* * * * * echo "cron mail test from $(hostname)"` to the crontab you're fixing (on cronie, restart crond first), wait a minute, confirm the mail arrived, and remove the entry. If delivery fails later, Debian's cron logs `mailed N bytes of output but got status 0x... from MTA` ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L672)).

## Why Mail Is a Poor Failure Signal Anyway

Working mail tells you what a job printed. It doesn't reliably tell you a job failed:

- **Success output is noise.** Cron mails any output, whatever the exit status, cronie's `-n` aside. Debian's cron marks the subject `(failed)` only when `-L` includes 4 ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L476)). A job that prints a line every run sends mail every run, and people learn to filter it.
- **Silent failures send nothing.** A job that exits 1 without printing, or whose output goes to `/dev/null` or a file, sends no mail.
- **A job that never starts sends nothing.** If cron stopped, the crontab was replaced, or the server is down, there's no run, so there's no output. [Cron job not running](https://cronradar.com/blog/cron-job-not-running) covers those causes.
- **Delivery fails quietly.** A rejected message becomes one line in the journal.

Catching those needs a check that expects each run and alerts when its report doesn't arrive. With CronRadar, a wrapper reports `start`, then `complete` or `fail` with the job's exit code, as in the [dead man's switch guide](https://cronradar.com/blog/cron-dead-mans-switch). A `fail` alerts immediately, and a run that doesn't report by its scheduled time plus the grace period turns the monitor `critical`. There is no CronRadar SDK or auto-discovery for crontab, so each job reports with its own HTTP check-in. CronRadar sends alerts itself, by email, Slack, webhook, Discord, or PagerDuty, so they don't depend on the server's mail setup. Keep the log file from Fix 1 for the details.

## Frequently Asked Questions

### What does "No MTA installed, discarding output" mean?

Debian's and Ubuntu's cron logs it when a job finished with output and `/usr/sbin/sendmail` doesn't exist. Cron would have emailed the output to `MAILTO` or the crontab's owner; with no mailer, it deletes it instead. The job itself is unaffected.

### Did my cron job run if I see this message?

Yes. Cron logs it after the job exits, and only when the job printed something. It doesn't say whether the job succeeded, and the discarded output may have been an error message. Redirect the output to keep it, and start cron with `-L 15` to log failures with their exit status.

### How do I stop cron from sending email?

Put `MAILTO=""` at the top of the crontab; it applies to every entry below it, and on Debian and Ubuntu it also stops the "No MTA" message. To quiet one entry, redirect its output, preferably to a file rather than `/dev/null`, so errors are kept.

### How do I send cron output to a log file?

Append `>> /var/log/app/nightly-report.log 2>&1` to the entry. `>>` appends, `2>&1` puts errors in the same file, and the directory must be writable by the crontab's owner. To use the journal instead, run the job as `systemd-cat -t nightly-report /srv/app/bin/nightly-report`.

### Do I need a mail server for cron?

No. Cron runs jobs without one; an MTA matters only if you want job output by email. A relay-only MTA such as msmtp-mta, or Postfix as a satellite system, is enough, and nothing on the server needs to accept incoming mail.
