If you're using self-hosted GitHub Actions runners, you might have noticed they can be surprisingly CPU-intensive, even when idle. A closer look reveals that a single runner can peg a CPU core at 100% utilization. This isn't a bug in your workflow; it's a deliberate design choice in the runner's sleep mechanism.
#The problem: Busy-waiting
Instead of using standard, efficient sleep functions provided by the operating system, the GitHub Actions runner employs a "busy-waiting" loop. This means it continuously checks the system clock in a tight loop to wait for a specified duration to pass. While this approach ensures compatibility across a wide range of environments, including some without traditional sleep functions, it's highly inefficient for the vast majority of systems, like standard Linux or Windows VMs. This constant checking keeps the CPU core fully active, leading to wasted resources, increased heat, and potentially reduced performance for other tasks on the same machine.
#The solution: A smarter sleep script
Fortunately, we can address this by replacing the runner's default sleep script with a more intelligent version. The goal is to use the most efficient sleep method available on the system. Here's a script that prioritizes standard sleep commands and provides fallbacks, ensuring both efficiency and compatibility.
Navigate to the folder where your runner is installed and edit the safe_sleep.sh file. Replace its content with the following:
Shell
#!/bin/bash
# Use the native 'sleep' command if available (most efficient)
if [ -x "$(command -v sleep)" ]; then
sleep $1
exit 0
fi
# Fallback to 'read' with a timeout, a bash built-in
if [[ -n "$BASH_VERSINFO" && "${BASH_VERSINFO[0]}" -ge 4 ]]; then
read -rt "$1" <> <(:) || :
exit 0
fi
# A creative fallback using 'ping'
if [ -x "$(command -v ping)" ]; then
ping -c $1 127.0.0.1 > /dev/null
exit 0
fi
# Original busy-waiting loop as a last resort
SECONDS=0
while [[ $SECONDS != $1 ]]; do
:
done
This script first tries to use the standard sleep command. If that's not available, it attempts to use read with a timeout, which is a shell built-in and still more efficient than busy-waiting. As another fallback, it uses ping. Only if none of these are available does it revert to the original busy-waiting loop.
#A word of caution
GitHub Actions runners can update automatically. When an update occurs, your changes to safe_sleep.sh may be overwritten. To ensure your fix persists, you could set up a cron job or a scheduled task to re-apply this change periodically.
#Additional resources
For more technical details and community discussion on this topic, you can refer to these GitHub issues and pull requests:
Do you have a question or a suggestion about this post? Contact me!