<!-- category: hidden-gems -->
Sub-Millisecond Sleep with select()
Perl'ssleep only does whole seconds:
Need to wait 50 milliseconds? Too bad.sleep 1; # waits 1 second sleep 0.5; # ALSO waits 1 second (truncated to integer)
sleep(0) is zero seconds, sleep(1) is one second. There is nothing in between. At least, not with sleep.
But there's this:
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.select(undef, undef, undef, 0.05); # 50 milliseconds
You are abusing the I/O multiplexing system call as a portable sub-second timer. Welcome to Perl.
Part 1: WHY SLEEP IS BROKEN
Thesleep function takes an integer. From perldoc -f sleep:
It truncates.sleep EXPR Causes the script to sleep for EXPR seconds, or forever if no EXPR. Returns the number of seconds actually slept.
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
Theselect 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);
With no file descriptors to watch, the only thing that can happen is the timeout. SoARG 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
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.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.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
# 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;
Close enough for government work.Requested: 50ms, Got: 50.2ms
Part 4: RATE LIMITING
The most common real-world use. You have an API that allows 10 requests per second. Space them out:Or more precisely, 20 requests per second:my @urls = get_url_list(); for my $url (@urls) { my $response = fetch($url); process($response); select(undef, undef, undef, 0.1); # 100ms between requests }
Withoutfor my $item (@work_queue) { process($item); select(undef, undef, undef, 0.05); # 50ms = 20/sec }
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.A progress bar with smooth updates:#!/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";
Usingmy $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";
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:Without sub-second sleep, you'd either poll once per second (laggy) or busy-loop (CPU hog). The# 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";
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 isTime::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?
For production code, useREASON 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
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
Theselect hack is perfect for one-liners where you don't want to pull in a module:
That typewriter effect prints each character of a file with a 30ms delay. It's three# 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"'
select arguments away from being a fun party trick.
Part 9: THE FOUR-ARGUMENT WEIRDNESS
Why four arguments? Becauseselect in Perl does double duty.
The one-argument select sets the default output filehandle:
The four-argumentselect(STDERR); # now print goes to STDERR print "This goes to STDERR\n"; select(STDOUT); # back to normal
select is the I/O multiplexer:
Two completely different functions sharing the same name. The parser distinguishes them by argument count. One argument: filehandle selector. Four arguments: I/O multiplexer.select($readable, $writable, $errors, $timeout);
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:
Theuse 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!"; }
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
Is.--. |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.
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