Alarm + Eval Timeouts
Your script hangs. A network request never comes back. A database query runs for eternity. A user walks away from a prompt and never types anything.You need a timeout. Perl gives you one with three tools from the standard library: alarm, eval, and %SIG.
Five seconds. Ifeval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm(5); my $result = slow_operation(); alarm(0); }; if ($@ =~ /timeout/) { say "Operation took too long. Moving on."; }
slow_operation() doesn't finish by then, it gets killed. No external modules. No threads. Just Unix signals and Perl's built-in exception handling.
Part 1: THE THREE PIECES
Each component has a job:The flow looks like this:PIECE WHAT IT DOES ------------------ ------------------------------------------ $SIG{ALRM} Signal handler - what to do when alarm fires alarm(N) Set a timer for N seconds eval { ... } Catch the die() so your script survives
The alarm(0) at the end is important. If the operation finishes in time, you cancel the alarm so it doesn't fire later and confuse something else.eval { set handler ──→ "die on SIGALRM" alarm(5) ──→ start countdown do work ──→ might finish, might not alarm(0) ──→ cancel countdown (if we got here) }; │ ├── work finished? ──→ $@ is empty, continue │ └── alarm fired? ──→ $@ contains "timeout\n"
Part 2: THE SIGNAL HANDLER
$SIG{ALRM} is Perl's way of registering a handler for the SIGALRM Unix signal. When the alarm goes off, this anonymous sub runs.local $SIG{ALRM} = sub { die "timeout\n" };
The local keyword is critical. It makes this handler temporary, scoped to the current block. When the eval exits, the old handler (if any) is restored. You're not permanently changing global signal behavior.
Why die and not just return? Because die is the only way to break out of whatever operation is running. A signal handler interrupts the current code. If that code is deep inside some I/O wait, die unwinds the stack back to the enclosing eval.
The \n at the end of the die string suppresses the "at script.pl line N" message. It's a Perl convention: if your die message ends with a newline, Perl assumes you've said everything you want to say.
Part 3: THE FULL PATTERN
Here's the robust version with proper error handling:Notice the re-throw. If the code inside eval dies for a reason other than our timeout, we don't want to swallow that error. Only catch what you expect.my $result; my $timed_out = 0; eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm(10); $result = do_something_slow(); alarm(0); # Cancel if we finished in time }; if ($@) { if ($@ eq "timeout\n") { $timed_out = 1; warn "Operation timed out after 10 seconds\n"; } else { die $@; # Re-throw unexpected errors } }
Part 4: TIMING OUT NETWORK REQUESTS
The classic use case. You're hitting an API and the remote server might be dead:Three seconds to connect. If the server is unreachable, you find out fast instead of waiting for the OS-level TCP timeout (which can be minutes).use IO::Socket::INET; my $socket; eval { local $SIG{ALRM} = sub { die "connect timeout\n" }; alarm(3); $socket = IO::Socket::INET->new( PeerAddr => 'api.example.com', PeerPort => 80, Proto => 'tcp', ) or die "connect failed: $!\n"; alarm(0); }; if ($@ =~ /timeout/) { say "Server didn't respond in 3 seconds. It's probably down."; } elsif ($@) { say "Connection error: $@"; } else { say "Connected!"; # Do stuff with $socket }
Part 5: TIMING OUT USER INPUT
Waiting for a human to type something. Humans are slow and unpredictable.Thirty seconds to answer. Walk away from the terminal and the script keeps going with a default. This is great for automated install scripts or interactive tools that need to work unattended too.my $answer; eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm(30); print "Enter your name (30 seconds): "; $answer = <STDIN>; chomp $answer if defined $answer; alarm(0); }; if ($@ =~ /timeout/) { say "\nToo slow. Using default."; $answer = "Anonymous"; } say "Hello, $answer!";
Part 6: TIMING OUT SYSTEM COMMANDS
External commands can hang forever. Wrap them:Fifteen seconds for a ping test. If the network is down and ping just sits there, you're not stuck.my $output; eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm(15); $output = `ping -c 5 192.168.1.1 2>&1`; alarm(0); }; if ($@ =~ /timeout/) { say "Ping took too long. Host might be unreachable."; } else { say "Ping results:\n$output"; }
For commands that might produce partial output before hanging:
A self-terminating tail. Read the log for 10 seconds, then move on.eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm(10); open my $fh, '-|', 'tail', '-f', '/var/log/syslog' or die "Can't run tail: $!\n"; while (<$fh>) { print "LOG: $_"; } alarm(0); }; # tail -f never exits on its own, so we always get here via timeout say "Stopped watching after 10 seconds.";
Part 7: NESTED TIMEOUTS
You can nest them. Inner timeouts override outer ones:Five seconds per host, thirty for the whole job. The local on $SIG{ALRM} makes nesting safe. Each level gets its own handler, and exiting the block restores the previous one.eval { local $SIG{ALRM} = sub { die "outer timeout\n" }; alarm(30); # 30 seconds for the whole batch for my $host (@hosts) { eval { local $SIG{ALRM} = sub { die "inner timeout\n" }; alarm(5); # 5 seconds per host check_host($host); alarm(0); }; if ($@ =~ /inner timeout/) { warn "$host: timed out\n"; } } alarm(0); }; if ($@ =~ /outer timeout/) { warn "Entire batch took too long!\n"; }
Be careful though. alarm() is a single global timer. Setting alarm(5) inside the loop resets the outer alarm(30). When the inner alarm(0) cancels, the outer timer is gone too.
For truly independent timers, you'd need something like Time::HiRes or a different approach entirely. But for sequential operations, the nesting pattern works fine.
Part 8: GOTCHAS
Sleep interacts with alarm. Both use the same underlying Unix mechanism. If you call sleep() inside an alarmed block, they interfere. Useselect(undef, undef, undef, $seconds) for sub-second delays instead.
Windows doesn't have real signals. alarm() is emulated on Windows and might not interrupt blocking I/O. On Unix systems, this pattern is rock solid.
$@ can be clobbered. If code between the eval and the $@ check calls something that uses eval internally, $@ might get overwritten. Check it immediately:
Die in signal handlers can be tricky. Some operations (like system calls deep in C libraries) might not respond cleanly to die from a signal handler. Most pure-Perl and standard I/O operations are fine.eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm(5); do_work(); alarm(0); }; my $err = $@; # Capture immediately! # Now safe to call other code log_something(); if ($err =~ /timeout/) { handle_timeout(); }
Part 9: A REUSABLE WRAPPER
Wrap the pattern into a sub so you don't repeat it everywhere:Usage:sub with_timeout { my ($seconds, $code) = @_; my $result; eval { local $SIG{ALRM} = sub { die "timeout\n" }; alarm($seconds); $result = $code->(); alarm(0); }; if ($@ =~ /timeout/) { return (undef, 'timeout'); } elsif ($@) { return (undef, $@); } return ($result, undef); }
Clean. Reusable. The timeout logic lives in one place.my ($data, $err) = with_timeout(5, sub { fetch_from_api('https://api.example.com/data'); }); if ($err) { say "Failed: $err"; } else { process($data); }
Part 10: DATABASE QUERY TIMEOUTS
Database queries are another prime candidate. An unoptimized query against a large table can run for minutes:Ten seconds for a query. If it's still running after that, something is wrong and you need to investigate, not wait. The empty arrayref fallback lets the rest of your script continue gracefully.use DBI; my $dbh = DBI->connect("dbi:Pg:dbname=production", $user, $pass); my $rows; eval { local $SIG{ALRM} = sub { die "query timeout\n" }; alarm(10); my $sth = $dbh->prepare("SELECT * FROM big_table WHERE complex_condition"); $sth->execute(); $rows = $sth->fetchall_arrayref({}); alarm(0); }; if ($@ =~ /timeout/) { warn "Query took more than 10 seconds. Check your indexes.\n"; $rows = []; } say "Got " . scalar(@$rows) . " rows";
Note that this kills the Perl-side wait, but the database query might keep running on the server. For true query cancellation, use your database's native timeout settings too.
Part 11: WHEN TO USE THIS
The alarm/eval pattern is the right tool when:For sub-second timeouts, look at Time::HiRes and its* You're on a Unix system * The operation might block indefinitely * You want whole-second granularity * You don't want to pull in external modules * The blocking code is a single call (not a complex state machine)
ualarm() function. For event-driven code with many concurrent timeouts, look at IO::Async or Mojo::IOLoop.
But for the everyday "don't let this script hang" problem? alarm + eval + $SIG{ALRM} is the Perl sysadmin's best friend. Three lines of setup, zero dependencies, and your script never hangs again.
alarm(5) | +-----+-----+ | | finish RING! in time | | die "timeout" alarm(0) | | eval catches continue | | handle it v v [done] [done] .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Created By: Wildcard Wizard. Copyright 2026