The Hidden Cost of date in Bash Scripts
by Anton Van Assche - 10 min read
If you have ever written a quick shell script, chances are you
reached for the date command to print a timestamp. It
works, it is familiar, and it is everywhere. But if you benchmark it
against Bash's built-in printf, the difference in speed
is hard to ignore.
Consider the following loops:
time for i in {1..10000}; do date > /dev/null; done
and
time for i in {1..10000}; do printf '%(%F %T)T\n' -1 > /dev/null; done
The loop that uses date usually takes several seconds,
while the printf loop often finishes in a fraction of a
second. At first glance, both commands do the same thing - print the
current time - so why is one so much slower than the other? The
explanation lies in how Unix processes work and in how Bash decides
what to run.
Processes and External Programs
When you type a command in Bash, the shell has to decide whether
that command is something it already knows how to run, called a
builtin, or whether it is an external program stored somewhere in
your filesystem. The printf you are using in the second
loop is a builtin: it lives inside the Bash process itself. By
contrast, date is an external program, usually
installed as /bin/date.
Running an external program like date requires a
surprising amount of work. First, Bash searches through the
directories listed in your $PATH until it finds the
correct executable file. Then it calls fork() to create
a child process, which is essentially a duplicate of the shell.
Inside this child process, Bash immediately calls
exec(), replacing the child's memory with the code of
the date program. Only then does
date begin to run. When it finishes, the shell must
wait for it and collect its exit status.
You can picture the process like this:
Parent shell (bash)
|
|-- fork() --------------------------.
| |
v v
Parent keeps running Child process
(waiting for child) exec("/bin/date")
|
v
date prints output
This overhead is tiny if you run date once. But in a
loop that executes thousands of times, the cost of forking and
executing a new process dominates the runtime.
Builtins and In-Process Execution
The story is different with printf. Because it is a
builtin, Bash does not need to fork or exec anything. The shell
simply calls a C function it already has loaded in memory. All the
work happens inside the existing Bash process. There is no process
management, no context switching, and no waiting for a child to
complete.
Parent shell (bash)
|
|-- directly calls printf builtin
|
v
printf formats and prints string
The result is that Bash can print thousands of timestamps with
printf in the time it takes date to spawn and execute a
handful of external processes.
Tracing the Difference with strace
You can actually observe this overhead with strace.
When Bash runs a builtin, the only execve you see is
the one that launches Bash itself. When Bash runs an external
program like date, there is an execve for
Bash and another one for the external binary:
$ strace -e execve bash -c 'date' |& grep execve
execve("/usr/bin/bash", ["bash", "-c", "date"], ...) = 0
execve("/usr/bin/date", ["date"], ...) = 0
Compare that with a builtin like printf. The only
execve here is the one that started Bash itself - there
is no second process created, because the builtin runs entirely
inside the existing shell:
$ strace -e execve bash -c "printf '%(%F %T)T\n' -1" |& grep execve
execve("/usr/bin/bash", ["bash", "-c", "printf '%(%F %T)T\\n' -1"], ...) = 0
That missing second execve call is the key difference:
external commands like date always incur the cost of
starting a new process, while builtins like printf stay
entirely in-process.
Measuring the Difference
The performance gap between printf and
date becomes immediately apparent when we measure them
across different loop counts. To illustrate, I ran both commands in
loops of 100, 1,000, and 10,000 iterations and recorded the
real, user, and sys times
using the time command.
First, here are the benchmarks for the Bash builtin
printf:
| Iterations | real | user | sys |
|---|---|---|---|
| 100 | 0m0.005s | 0m0.002s | 0m0.003s |
| 1000 | 0m0.018s | 0m0.015s | 0m0.003s |
| 10000 | 0m0.141s | 0m0.110s | 0m0.030s |
For comparison, here are the same benchmarks using the external
date command:
| Iterations | real | user | sys |
|---|---|---|---|
| 100 | 0m0.099s | 0m0.039s | 0m0.063s |
| 1000 | 0m0.773s | 0m0.314s | 0m0.475s |
| 10000 | 0m8.370s | 0m3.175s | 0m5.327s |
As these results show, printf is consistently tens to
hundreds of times faster than date, particularly as the
number of iterations grows. The difference is not just in overall
runtime: the user and sys times reveal
why. While printf executes almost entirely in
user mode within the Bash process,
date spends a significant portion of its time in
sys mode, handling the overhead of creating and
managing new processes for each call.
This demonstrates that for high-frequency timestamp printing or
other repetitive tasks within Bash, builtins like
printf offer substantial performance advantages over
external commands like date.
Why date Still Matters
If printf is so much faster, why is
date still around and widely used? The first reason is
portability. The %T extension is specific to Bash and
does not exist in the POSIX specification for printf.
If you write a script that is meant to run under
/bin/sh on systems where Bash is not guaranteed, you
cannot rely on it.
For example, on dash (a lightweight POSIX shell),
printf simply fails:
$ dash -c 'printf "%(%F %T)T\\n" -1'
dash: 1: printf: %T: invalid directive
The secode reason is compatibility within Bash itself. The usage of
printf paired with the %T format specifier
is a relatively recent addition to Bash, introduced in version 4.2.
Many existing scripts and systems still run on older versions of
Bash that do not support this feature. In such cases, using
date remains the only viable option for printing
formatted timestamps.
Another reason is features. date can do far more than
simply print the current timestamp. It can parse natural language
expressions like yesterday or next Monday, convert between time
zones, or apply complex arithmetic to dates.
Finally, there is the matter of familiarity. date has
been part of the Unix toolbox for decades, and most people reach for
it without thinking about alternatives.
The Bottom Line
The lesson here is simple: Bash builtins run in-process and avoid
the overhead of forking and executing new programs. External
commands like date require that overhead every time
they are called. If you are writing a script for Bash and care about
speed, prefer printf for printing timestamps. If you
need portability, complex date arithmetic, or simply the
long-established familiarity of date, then stick with
it. Both tools have their place, but now you know why one is so much
faster than the other.
References & Further Reading
Bash
man bash- Bash Manual-
man 3 printf-printf()Library Functions Manual
GNU Coreutils
man date
-
date
Command Manual
System Calls
-
man 2 fork-fork()System Call Manual -
man 2 execve-exec()System Call Manual -
man 2 wait-wait()System Call Manual
Library Functions
-
man 3 strftime-strftime()Library Functions Manual