perl.gg / hidden-gems

<!-- category: hidden-gems -->

Sub-Millisecond Sleep with select()

2026-05-07

Perl's sleep only does whole seconds:
sleep 1; # waits 1 second sleep 0.5; # ALSO waits 1 second (truncated to integer)
Need to wait 50 milliseconds? Too bad. sleep(0) is zero seconds, sleep(1) is one second. There is nothing in between. At least, not with sleep.

But there's this:

select(undef, undef, undef, 0.05); # 50 milliseconds
Four arguments. Three undefs. One floating-point number. The ugliest timer in all of programming. And it works everywhere, on every platform, with no modules, no imports, no dependencies.

You are abusing the I/O multiplexing system call as a portable sub-second timer. Welcome to Perl.

Part 1: WHY SLEEP IS BROKEN

The sleep function takes an integer. From perldoc -f sleep:
sleep EXPR Causes the script to sleep for EXPR seconds, or forever if no EXPR. Returns the number of seconds actually slept.
It truncates. sleep(0.9) sleeps for zero seconds. sleep(1.1) sleeps for one second. There is no way to sleep for half a second using the built-in.

This made sense in the 1970s when Unix sleep(3) took an integer. But in 2026, when you need to rate-limit API calls to 20 per second, or poll a socket every 100ms, or animate a terminal spinner, whole seconds are absurdly coarse.

Part 2: THE SELECT() HACK

The select function with four arguments is Perl's interface to the select(2) system call. Its real job is I/O multiplexing. You give it sets of file descriptors to watch and a timeout, and it blocks until something is readable, writable, has an error, or the timeout expires.

The hack: give it no file descriptors and just a timeout.

select(undef, undef, undef, $seconds);
ARG 1: readable file descriptors -> undef (none) ARG 2: writable file descriptors -> undef (none) ARG 3: exception file descriptors -> undef (none) ARG 4: timeout in seconds -> your wait time
With no file descriptors to watch, the only thing that can happen is the timeout. So select waits for the specified duration and returns. Congratulations, you've built usleep() from spare parts.

Part 3: PRECISION

How precise is it? The fourth argument is a floating-point number in seconds.
select(undef, undef, undef, 1); # 1 second select(undef, undef, undef, 0.5); # 500 milliseconds select(undef, undef, undef, 0.1); # 100 milliseconds select(undef, undef, undef, 0.001); # 1 millisecond select(undef, undef, undef, 0.0001); # 100 microseconds
The actual resolution depends on your OS timer. Most modern systems get you down to about 1 millisecond reliably. Sub-millisecond is possible but not guaranteed. You might ask for 100 microseconds and get 200. But for the kind of work where you need sub-second timing (rate limiting, animation, polling), millisecond accuracy is plenty.
# measure the actual sleep time use Time::HiRes qw(time); my $t0 = time(); select(undef, undef, undef, 0.050); # request 50ms my $elapsed = time() - $t0; printf "Requested: 50ms, Got: %.1fms\n", $elapsed * 1000;
Requested: 50ms, Got: 50.2ms
Close enough for government work.

Part 4: RATE LIMITING

The most common real-world use. You have an API that allows 10 requests per second. Space them out:
my @urls = get_url_list(); for my $url (@urls) { my $response = fetch($url); process($response); select(undef, undef, undef, 0.1); # 100ms between requests }
Or more precisely, 20 requests per second:
for my $item (@work_queue) { process($item); select(undef, undef, undef, 0.05); # 50ms = 20/sec }
Without select, you'd need Time::HiRes or a busy loop. The select hack gives you sub-second pacing with zero dependencies.

Part 5: TERMINAL ANIMATION

Spinners, progress bars, typewriter effects. All need sub-second timing.
#!/usr/bin/env perl use strict; use warnings; my @frames = qw(| / - \\); my $i = 0; for (1 .. 40) { print "\rProcessing... $frames[$i % 4] "; $i++; select(undef, undef, undef, 0.1); # 100ms per frame } print "\rDone! \n";
A progress bar with smooth updates:
my $total = 100; for my $n (1 .. $total) { my $pct = int($n / $total * 100); my $bar = '=' x ($pct / 2) . '>' . ' ' x (50 - $pct / 2); printf "\r[%s] %3d%%", $bar, $pct; do_work($n); select(undef, undef, undef, 0.03); # smooth animation } print "\n";
Using sleep(1) here would make the animation jagged and painfully slow. select with 30ms intervals makes it silky.

Part 6: POLLING LOOPS

Waiting for something to happen without burning CPU:
# wait for a file to appear (poll every 200ms) my $target = '/tmp/job_done.flag'; my $waited = 0; my $max = 30; # max 30 seconds while (!-e $target) { select(undef, undef, undef, 0.2); # 200ms $waited += 0.2; if ($waited >= $max) { die "Timed out waiting for $target\n"; } } say "File appeared after ${waited}s";
Without sub-second sleep, you'd either poll once per second (laggy) or busy-loop (CPU hog). The select hack hits the sweet spot: responsive but efficient.

A more practical version that waits for a process to finish:

sub wait_for_pid { my ($pid, $timeout) = @_; $timeout //= 60; my $elapsed = 0; while (kill 0, $pid) # process still alive? { select(undef, undef, undef, 0.25); # check 4 times/sec $elapsed += 0.25; die "Process $pid still running after ${timeout}s\n" if $elapsed > $timeout; } return $elapsed; }

Part 7: TIME::HIRES ALTERNATIVE

The clean way to do sub-second sleep is Time::HiRes:
use Time::HiRes qw(usleep nanosleep sleep); sleep(0.5); # now accepts fractional seconds! usleep(50_000); # 50,000 microseconds = 50ms nanosleep(1_000); # 1,000 nanoseconds = 1 microsecond
Time::HiRes has been a core module since Perl 5.7.3 (2002). It's almost certainly available on your system. It overrides sleep() to accept fractions, and adds usleep() and nanosleep() for explicit precision.

So why would anyone use the select hack?

REASON SELECT TIME::HIRES ---------------------------- ------ ----------- Always available (no use) yes needs use Works in one-liners yes -MTime::HiRes Zero characters of setup yes no Self-documenting no yes Clean API no yes Nanosecond precision no yes Won't confuse code reviewers no yes
For production code, use Time::HiRes. For quick scripts, one-liners, and situations where you don't want any imports, select gets the job done.

Part 8: ONE-LINERS

The select hack is perfect for one-liners where you don't want to pull in a module:
# blink a message perl -e 'while(1){print "\rALERT! ";select(undef,undef,undef,0.5);print "\r ";select(undef,undef,undef,0.5)}' # slow cat (typewriter effect) perl -ne 'for(split//){print;select(undef,undef,undef,0.03)}' file.txt # countdown timer perl -e 'for(reverse 1..10){printf "\r%2d ",$_;select(undef,undef,undef,1)}print "\rGO!\n"'
That typewriter effect prints each character of a file with a 30ms delay. It's three select arguments away from being a fun party trick.

Part 9: THE FOUR-ARGUMENT WEIRDNESS

Why four arguments? Because select in Perl does double duty.

The one-argument select sets the default output filehandle:

select(STDERR); # now print goes to STDERR print "This goes to STDERR\n"; select(STDOUT); # back to normal
The four-argument select is the I/O multiplexer:
select($readable, $writable, $errors, $timeout);
Two completely different functions sharing the same name. The parser distinguishes them by argument count. One argument: filehandle selector. Four arguments: I/O multiplexer.

This is classic Perl. One function name, two meanings, distinguished by context. Love it or hate it, you can't say it's boring.

If you're using the four-argument form for its actual purpose (watching file descriptors), it looks like this:

use IO::Select; my $sel = IO::Select->new(); $sel->add(\*STDIN); if ($sel->can_read(5.0)) # wait up to 5 seconds { my $line = <STDIN>; chomp $line; say "You typed: $line"; } else { say "You took too long!"; }
The IO::Select module wraps the raw select call in something readable. But when all you want is a timer, the raw four-argument form with three undefs is the shortest path.

Part 10: THE VERDICT

.--. |o_o | "Three undefs and a float |:_/ | walk into a function call..." // \ \ (| | ) /'\_ _/`\ \___)=(___/ sleep(1) = 1 second select(undef,undef,undef,0.001) = 1 millisecond Time::HiRes::usleep(1000) = 1 millisecond (clean) Same result. Different dignity.
Is select(undef,undef,undef,0.001) ugly? Absolutely. Is it a hack? Without question. Does it work on every Perl installation, on every platform, with no modules, no imports, no setup? Yes.

The select microsleep is the duct tape of Perl timing. It's not pretty. It's not self-documenting. It will make your coworkers squint at the screen. But it's been working since Perl 4 and it will keep working long after we're all gone.

For production code, use Time::HiRes. For quick scripts and one-liners, three undefs and a float is all you need.

perl.gg