Crontab Errors: "bad minute", "premature EOF", and Other Messages
Contents
"/tmp/crontab.Xq3fLa/crontab":4: bad minute followed by errors in crontab file, can't install. means crontab couldn't parse the first field of a line in the file you saved, so it refused the whole file and left your installed crontab unchanged. Debian and Ubuntu print that second line; cronie, the cron on Fedora, RHEL, and Arch, prints Invalid crontab file, can't install. instead. BusyBox's crontab, on Alpine, installs anything, and its errors appear later in crond's log as user root: parse error at ....
Below, each message is traced to the source line that prints it, with what triggers it and the fix. This guide covers cronie 1.7.2, the current release on GitHub; Debian's cron 3.0pl1-197, the version in Debian 13 (tracker.debian.org); and BusyBox 1.37.0, the version in Alpine 3.24 (APKBUILD) (checked October 30, 2026). Debian testing's cron 3.0pl1-212 prints the same messages plus a pathname too long check on the file argument (patch), and BusyBox 1.38.0's crond.c prints the same messages as 1.37.0's. Ubuntu 24.04's cron 3.0pl1-184ubuntu2 prints all of Debian's except unsafe mail (Launchpad). RHEL 9's cronie 1.5.7 carries Red Hat backports of the step-size warning, the -n job option, and the 10,000-entry limit (CentOS Stream 9 cronie.spec), so it prints the same messages as cronie 1.7.2. Editing crontabs and checking one without installing it are covered in the crontab guide; to find out which cron you run, see cron implementations.
Which Cron Printed It#
| Message | Printed by | When |
|---|---|---|
"FILE":N: bad minute, bad hour, ... bad command |
cronie and Debian crontab |
A line fails to parse |
Invalid crontab file, can't install. |
cronie crontab |
After any parse error |
errors in crontab file, can't install. |
Debian and Ubuntu crontab |
After any parse error |
"FILE":N: premature EOF |
cronie crontab |
The last line has no newline |
new crontab file is missing newline before EOF, can't install. |
Debian and Ubuntu crontab |
The last line has no newline |
Do you want to retry the same edit? (Y/N) |
cronie crontab -e |
After a refused edit |
Do you want to retry the same edit? (y/n) |
Debian crontab -e |
After a refused edit |
(CRON) bad minute (/etc/cron.d/app) |
cronie crond |
Loading a file with a bad line, which it skips |
Error: bad minute; while reading /etc/cron.d/app |
Debian cron |
Loading a file with a bad line, then ignoring that file |
user root: parse error at 60 |
BusyBox crond |
Loading a crontab with a field it can't read |
Sources: cronie entry.c, crontab.c, and user.c; Debian entry.c, crontab.c, and the patches cited below; BusyBox crond.c.
How crontab Reports a Bad Line#
cronie and Debian check the file when you save from crontab -e, run crontab file, or pipe into crontab -. On the first line that fails, both print the file name, a line number, and the reason with the same format string, "%s":%d: %s (cronie crontab.c, Debian crontab.c), then the summary line:
"/tmp/crontab.Xq3fLa":4: bad minute
Invalid crontab file, can't install.- The file name is crontab's temporary copy when you used
crontab -e(by default/tmp/crontab.XXXXXXon cronie and/tmp/crontab.XXXXXX/crontabon Debian), the path you passed, or-for standard input (cronie, L366-L369; Debian). - Only the first error is reported. Both stop checking at the first bad line (cronie, Debian). Fix it and check again to find the next one.
- Debian's line number is usually one too low. crontab prints its line count minus one (crontab.c), which is right only once the bad line's newline has been read. Most errors, such as an out-of-range value, a bad name, or a
*where the command should start, are found earlier in the line and printed before crontab reads its newline (entry.c), so they're reported against the line above. Bug #571176, still open, shows both cases:PENG AMon line 2 reported as line 1, and a loneBLAon line 4 reported correctly. Check the line after the one named. - Nothing is installed.
crontab fileexits with status 1 (cronie, Debian), andcrontab -easks whether to retry, covered below.
bad minute, bad hour, bad day-of-month, bad month, bad day-of-week#
These five name the field crontab was parsing when it gave up (cronie entry.c). Both implementations accept the same values in each field (cronie macros.h, Debian cron.h); cron expression syntax lists them and the operators. What triggers the messages in practice:
- A value out of range, such as
60for a minute,24for an hour,0or32for a day of month,13for a month, or8for a day of week. - A full name. Names are three letters in any case, so
Janandmonwork andJanuaryandMondaydon't (cronie entry.c, Debian cron.h). - Quartz or Spring syntax.
?,L,W,5L, andMON#1aren't cron syntax; cron last day of month shows how to get the effect ofL. Debian reads every letter and digit into the value, soL,W, and5Lfail there too (entry.c). After a valid value, though, it skips any other characters up to the next blank (L344), so0 9 * * MON#1installs there as every Monday, while cronie rejects it. - A seconds field. Spring and Quartz expressions begin with seconds, so every value lands one field early:
0 30 2 * * *, meant as 02:30:00, puts30in the hour field and fails withbad hour. - A step without a range.
5/15isbad minutein both (cronie entry.c, Debian patch); write5-59/15. BusyBox accepts5/15without a message and runs only at minute 5 (crond.c). - A step of zero, as in
*/0(cronie, Debian patch). - A line that isn't a valid variable. Any line that isn't blank, a comment, or
NAME=valueis parsed as a job, so its first word is read as the minute field (cronie env.c, Debian patch).export PATH=/usr/local/bin:/usr/bin:/bin,source ~/.profile, andMAILTO [email protected]without its=all fail asbad minute. WritePATH=/usr/local/bin:/usr/bin:/bin; cron environment variables covers what a variable line can and can't do.
The Step-Size Warning Is Not an Error#
Warning: Step size 90 higher than possible maximum of 59cronie and Debian print this for a step larger than the span of the range it steps through, which for * is the whole field, then install the line anyway (cronie entry.c, Debian patch). */90 * * * * then runs at minute 0 of every hour, because the next step lands past 59. A 90-minute interval takes two lines:
0 0-21/3 * * * /opt/app/sync.sh
30 1-22/3 * * * /opt/app/sync.shThe first runs at 00:00, 03:00, and so on through 21:00; the second at 01:30, 04:30, and so on through 22:30.
bad command#
crontab doesn't check that the command exists. cronie prints bad command when nothing follows the time fields, or when the command would start with * (entry.c); Debian's checks only for the * (Better-timeskip-handling.patch). cronie's source says only that the * check catches "a common typo". Among the lines it rejects is a six-field expression whose sixth field is *, like this Spring one:
# "Every 5 minutes" in Spring's six-field syntax: rejected with bad command
0 */5 * * * * /opt/app/poll.sh
# The five-field cron equivalent
*/5 * * * * /opt/app/poll.shOnly a * is caught. Any other sixth field becomes the start of the command, so the Quartz expression 0 0 2 * * ? /opt/app/report.sh installs without an error and runs the command ? /opt/app/report.sh at 00:00 on the 2nd of every month. Cron's smallest interval is a minute; cron every 30 seconds covers the workarounds.
Other Messages That Refuse a Crontab#
| Message | Printed by | Cause and fix |
|---|---|---|
bad time specifier |
cronie, Debian | @ followed by anything other than reboot, yearly, annually, monthly, weekly, daily, midnight, or hourly, in lowercase; @Daily and @every fail (cronie, Debian) |
bad option |
cronie | A - option before the command other than a single -n, or a line starting with - in a non-root user's crontab (entry.c, L420-L448) |
unsafe mail |
Debian 13 | A MAILTO or MAILFROM value that doesn't start with a letter or digit, or contains a character other than letters, digits, and @!:%-.,_+=/. [email protected], [email protected] fails on the space; remove it (check_e_mails.patch, safe_p) |
command too long |
Debian | A command of 999 characters or more; move it into a script (Dont-silently-truncate-commands.patch) |
crontab is too long; maximum number of lines is 10000. |
Debian | More than 10,000 lines (Enforce-maximum-crontab-line-count.patch) |
There are too many entries in the crontab file. Limit: 10000 |
cronie | More than 10,000 entries; a matching message caps variables at 1,000 (crontab.c) |
premature EOF and the Other Missing-Newline Messages#
A crontab whose last line has no newline is refused by both crontab commands; the crontab guide explains why and how to test a file for it. The message depends on which program reads the file:
| Reader | Message | Result |
|---|---|---|
cronie crontab |
"FILE":N: premature EOF, then Invalid crontab file, can't install. |
Refused; N is the last line |
Debian crontab |
new crontab file is missing newline before EOF, can't install. |
Refused |
cronie crond |
(CRON) missing newline before EOF (/etc/cron.d/app) |
The last line is skipped |
Debian cron |
(*system*app) ERROR (Missing newline before EOF, this crontab file will be ignored) |
The whole file is ignored |
| BusyBox | None | The last line is read normally |
Sources: cronie crontab.c and user.c; Debian crontab-detect-missing-eol.patch and Abort-processing-of-crontabs-with-errors.patch.
A comment as the last line triggers none of these, because both parsers read a comment through to the end of the file before looking for another entry (cronie misc.c, Debian env.c). The crontab guide's install script adds the newline only when it's missing.
Do You Want to Retry the Same Edit?#
cronie and Debian ask this after crontab -e saves a file that fails the check, missing newline included; the crontab guide covers what y and n do, and that BusyBox never asks. cronie's prompt ends in (Y/N) and Debian's in (y/n), both take either case, and any other answer prints Enter Y or N and asks again (cronie crontab.c, Debian crontab.c, Improve-retry-prompt.patch). After n, crontab prints crontab: edits left in /tmp/crontab.Xq3fLa (on Debian, a path ending in /crontab); fix that file and install it with crontab /tmp/crontab.Xq3fLa.
The installed crontab is untouched until a check passes. Two consequences for automation: crontab -e exits with status 0 even after n (cronie, Debian), and neither implementation stops asking when standard input is closed. cronie reprints the prompt when the read fails, and Debian answers its own empty read with Enter Y or N, so a script that drives crontab -e through $EDITOR loops forever on a syntax error. Scripts should install with crontab file, which exits 1 instead; the crontab guide has a script that adds an entry that way without dropping the others.
When the Daemon Finds the Error#
Nothing checks /etc/crontab or /etc/cron.d files before the daemon reads them, and the same goes for a user crontab copied into the spool directory instead of installed with crontab. The daemon logs the reason and the file, but not the line:
| Daemon | Log messages | Effect |
|---|---|---|
| cronie crond | (CRON) bad minute (/etc/cron.d/app) |
That line is skipped; the rest runs |
| Debian cron | Error: bad minute; while reading /etc/cron.d/app, then (*system*app) ERROR (Syntax error, this crontab file will be ignored) |
Every job in the file stops |
Sources: cronie user.c and misc.c; Debian Logging-enhancements.patch and Abort-processing-of-crontabs-with-errors.patch. For /etc/crontab, Debian logs while reading /etc/crontab and (*system*); for a user's crontab, while reading crontab for user alice and (alice).
The user field has its own failures, since system crontab lines carry one and user crontabs don't. Debian reads the first word after the time fields as the account and logs bad username when no such account exists, which is what a line missing its user field usually produces (entry.c). cronie loads such a line and, at each scheduled time, logs getpwnam() failed - user unknown instead of running it (entry.c, job.c); with a one-word command, the line is bad command instead.
Find the messages in the journal. Debian's unit is cron (cron.service) and Fedora installs cronie's as crond (cronie.spec):
journalctl -u cron --since today | grep -E 'Error: |ERROR \(' # Debian, Ubuntu
journalctl -u crond --since today | grep -F '(CRON)' # Fedora, RHELTo find the line, check a copy of the file with crontab's own parser. This drops the user field from each job line, keeps every other line as it is so line numbers still match, and installs nothing:
# Run as root. Field 6 is the user on a time-field line; field 2 on an @ line.
awk '/^[[:space:]]*(#|$)/ || /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/ { print; next }
/^[[:space:]]*@/ { $2 = ""; print; next }
{ $6 = ""; print }' /etc/cron.d/app > /tmp/app-check.cronThen check the copy with your implementation's syntax-check option and delete it. The crontab guide's options table lists which versions have -n and -T, and what -n does on cronie instead, so run only your implementation's line:
crontab -n /tmp/app-check.cron && rm /tmp/app-check.cron # Debian, Ubuntu
crontab -T /tmp/app-check.cron && rm /tmp/app-check.cron # cronieThe copy can't reveal a missing final newline, because awk ends every line with one, or a missing user field, which Debian's log names as bad username and cronie's as getpwnam() failed - user unknown, or as bad command for a one-word command.
BusyBox: No Check, One Log Message#
BusyBox's crontab copies the file into place without parsing it (crontab.c), so it never prints a syntax error. crond parses the file when it next loads it, and for a field it can't read it logs one line (crond.c):
user root: parse error at 60root is the crontab's file name, which BusyBox uses as its user name (L445), and 60 is the field's text. The line stays loaded, and what the field then matches depends on where parsing stopped:
- An out-of-range number fills the whole field. The fill loop wraps around until a failsafe ends it (crond.c), so
60in the minute field runs every minute, and7in the day-of-week field runs every day, as cron implementations notes. - An unrecognized character leaves the field empty.
Lor?stops parsing before anything is set (crond.c). In the minute, hour, or month field, the line then never runs. In a day field, it never runs only if the other day field is*, because BusyBox runs a line when either day field matches:0 9 ? * MONstill runs every Monday (L336-L360, L912).
Other mistakes produce no message at all:
- A line with fewer than six fields, such as one with no command, is skipped (crond.c).
- An unknown
@keyword is skipped (L542). - A variable other than
MAILTO,SHELL, orPATHis skipped because it has fewer than six fields (L546-L547). One with six or more words, such asMSG=a b c d e f, is read as a job instead and logsparse error. - A crontab not owned by root is ignored (L440). One named after an account that doesn't exist is logged only at log level 7 or lower, and the default is 8 (L429-L431, L168, L178).
A non-root crontab is also cut off at MAXLINES, 256 by default, with user alice: too many lines (L103-L104, L451-L453). crond logs to syslog unless started with -L FILE, or with -d N, which logs to standard error (crond.c).
Catch the Jobs a Bad Crontab Stops#
A refused crontab keeps the old crontab, so a job you meant to add doesn't exist and a job you meant to change keeps its old schedule. On Debian and Ubuntu, one bad line in /etc/crontab, an /etc/cron.d file, or a crontab written outside crontab stops every job in that file, and the only trace is two log lines. Nothing on the host raises an alert.
A check-in monitor notices from outside. With CronRadar, each run reports start, then complete or fail; a fail opens an incident immediately, and a run that hasn't reported by its scheduled time turns its monitor warning, then critical once the grace period runs out. Alerts go by email, Slack, webhook, Discord, or PagerDuty. There's no CronRadar SDK or auto-discovery for crontab, so each job reports with its own HTTP check-in. Cron grace period has a wrapper script that reads the API key from a file only the job's account can read, sends it with HTTP Basic auth, and exits with the job's own status, so the key never appears in the crontab. CronRadar reads every schedule as five fields in UTC, so convert the crontab line's times to UTC, write @ shortcuts out as fields, and use 0, not 7, for Sunday. An entry that restricts both day fields needs two entries, each with its own job key, because CronRadar expects a run only on dates that match both fields, where cron runs on either (schedules).
A monitor created by the job's first check-in doesn't exist until that check-in arrives, so a line that never installed has nothing to go critical. Register each job before its first run with POST /sync in upsert mode instead: a monitor that never receives a check-in then turns critical, with a NeverExecuted alert, once its first expected time plus the grace period has passed.
Frequently Asked Questions#
What does "errors in crontab file, can't install" mean?#
Debian's or Ubuntu's crontab found a line it couldn't parse, printed it above this message as "FILE":LINE: reason, and kept your current crontab. Fix that line and install again; only the first bad line is reported per check. cronie prints Invalid crontab file, can't install. for the same thing.
What does "bad minute" mean in crontab?#
crontab couldn't read the first field of a line. Common causes are a value outside 0-59, a step without a range such as 5/15, and a line that is neither a job nor a valid NAME=value variable, such as export PATH=..., whose first word is read as the minute.
Why does crontab say "premature EOF"?#
cronie's crontab prints it when the file's last line has no newline. Add one with echo >> file and install again. Debian's crontab reports the same problem as new crontab file is missing newline before EOF, can't install.
How do I answer "Do you want to retry the same edit?"#
Answer y to reopen the editor with your changes and fix the line named above the prompt. Answer n to quit; crontab keeps your edits in the temporary file it names, and the installed crontab is unchanged.
Why doesn't crontab on Alpine report errors?#
BusyBox's crontab installs files without parsing them. crond reports a bad field later in its log, as user root: parse error at 60, and silently skips lines with no command, unknown @ keywords, and variable lines of fewer than six words other than MAILTO, SHELL, and PATH.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.