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