coderquill's inklings

Beyond the Pause: Exploring the Inner Workings of Python’s `sleep()`

Sleeping python

Table of Contents

sleep() might seem like the simplest command in Python—just tell your program to wait for a few seconds, and that’s it. But is it really that straightforward? Does the program just sit there, doing nothing, or is something more complex happening behind the scenes? How accurate is the sleep 💤?1

What happens when the system time itself changes when sleep is already executed?

What happens to sleep when the system is under a heavy CPU bound task?

What happens when sleep is interrupted by system interrupt, does it still finish the sleep duration?

If you’ve ever been curious about these and wanted to know what actually happens when you call sleep(), you’re in the right place. This isn’t just a "pause" button—it’s a carefully orchestrated process that involves multiple layers, from Python code all the way down to hardware-level instructions.

In this post, we’ll peel back those layers, explore different types of sleep, and uncover why Python’s sleep() might last longer than you expect—but never less. So buckle up, because we’re about to go deep into the world of timing, clocks, and operating system magic!💫


Synchronous vs Asynchronous Sleep: What’s the Difference?

sync vs async image

Before we dive into the details, let’s take a quick look at the two main types of sleep in Python: synchronous and asynchronous.

While both involve pausing execution, they work very differently.

Synchronous Sleep

Synchronous sleep, as in time.sleep(), halts the entire program for a set amount of time. During this period, nothing else happens—your program is completely blocked.

import time

print("Start")

time.sleep(3)  # Halts the program for 3 seconds

print("End")   # This line won't execute until after the sleep is over

Output

Start
End

In this example, the program will pause entirely for 3 seconds before moving on to the next statement. This kind of sleep affects everything, as no other code can execute during the wait.

Asynchronous Sleep

Asynchronous sleep, on the other hand, allows other parts of your program to continue executing while some tasks "sleep" in the background. It’s commonly used in asynchronous programming, where you don’t want to block the entire application.

import asyncio

async def main():
    print("Start")
    await asyncio.sleep(3)  # Pauses this task, but allows others to run
    print("End")

# Run the async function
asyncio.run(main())

Output

Start
End

Here, the program doesn’t block entirely. While the await pauses the execution of main() for 3 seconds, the event loop is free to run other tasks during this period.


Why Focus on Synchronous Sleep?

In this post, we’ll focus on synchronous sleep.\

Why?

Because it triggers actions that go beyond just Python code — into the operating system and hardware layers. Asynchronous sleep, in contrast, stays mostly within the application layer, managed by Python's event loop.

🛑 The Gotcha: Sleep Isn’t Always Exact2 😮

Here’s a catch: sleep durations aren’t always as precise as you might think. When you tell your program to sleep for 3 seconds, it could actually sleep for slightly more than that.

Lets see it with a very simple example -

import time 

# Record the start time 
start_time = time.perf_counter() 

# Sleep for 3 seconds 
time.sleep(3) 

# Record the end time 
end_time = time.perf_counter() 

# Calculate the actual sleep 
actual_sleep_duration = end_time - start_time 

print(f"Requested sleep: 3 seconds") 
print(f"Actual sleep duration: {actual_sleep_duration:.6f} seconds")

Output

Requested sleep: 3 seconds
Actual sleep duration: 3.007419 seconds

This is a simplest example, a more detailed example can be found here3

Why?

The reason is simple: Python’s time.sleep() (and other system-level sleep functions) depend on various factors like CPU scheduling, system load, and context switches. The operating system might have other processes running and needs to juggle those as well. As a result, your sleep could overshoot by a few milliseconds or more.

🛑 Gotcha : Sleep will never be shorter than what you request, but it could be longer, depending on the system’s state at the time.

Why Sleep Never Undershoots: Enter the ✨Monotonic Clock

While sleep() can overshoot, Python ensures it never sleeps for less time than specified. Lets verify this with an actual example -

import time
import signal

# Define a handler for the signal
def handle_interrupt(signum, frame):
    print("Signal received! Interrupt occurred during sleep.")

# Register the signal handler for SIGALRM
signal.signal(signal.SIGALRM, handle_interrupt)

# Set an alarm signal to go off after 1 second
signal.alarm(2)  # This will raise SIGALRM after 2 seconds

# Record the start time using the monotonic clock
start_time_monotonic = time.monotonic()
print("Going to sleep for 5 seconds...")

# Sleep for 5 seconds
time.sleep(5)

# Record the end time using the monotonic clock
end_time_monotonic = time.monotonic()

# Calculate the total time slept using the monotonic clock
actual_sleep_duration = end_time_monotonic - start_time_monotonic
print(f"Requested sleep: 5 seconds")
print(f"Actual sleep duration (monotonic clock): {actual_sleep_duration:.6f} seconds")

# Cancel any remaining alarms (if any)
signal.alarm(0)

Output

Going to sleep for 5 seconds...
Signal received! Interrupt occurred during sleep.
Requested sleep: 5 seconds
Actual sleep duration (monotonic clock): 5.002614 seconds

We can see the actual sleep is still 5 seconds but not less than 5 seconds even though we had an interrupt, how does this work? This is where the monotonic clock comes into play.

A monotonic clock always moves forward—it’s not affected by changes in system time. Unlike the regular system clock, which can jump forward or backward (e.g., when you manually change it or an NTP(Network Time Protocol ) sync happens), the monotonic clock guarantees that time only moves in one direction. This is why Python uses the monotonic clock for time.sleep().

Even if your system clock changes while the program is sleeping, the monotonic clock ensures that your program doesn’t wake up early. Here’s a rough outline of how it works:

Getting Curious: What’s Happening Under the Hood?

Now that we know Python’s sleep never undershoots and that the monotonic clock is key, the next question is: how is all of this implemented under the hood?

This is where the fun begins, let's dive into the actual python implementation for sleep. For the scope of this experiment, let's stick to cPython implementation 4 and assume the Unix environment.

Because sleep actually leverages underlying OS system calls, different operating systems need separate handlings for implementing sleep mechanisms and hence the actual python implementation for sleep seems a bit complicated.

I have simplified it below for clarity purpose, the complete implementation can be found here 5

do {
    int ret;

// Release the Global Interpreter Lock (GIL) to allow other threads to run
    
    Py_BEGIN_ALLOW_THREADS

    // Use nanosleep for sleeping with the timeout specified
    ret = nanosleep(&timeout_ts, NULL);
    err = errno;

// Re-acquire the Global Interpreter Lock (GIL) so the current thread can continue executing Python code
    Py_END_ALLOW_THREADS

    if (ret == 0) {
        // Sleep finished successfully
        break;
    }

    if (err != EINTR) {
        // If the error isn't an interrupt (signal), set an error and return
        errno = err;
        PyErr_SetFromErrno(PyExc_OSError);
        return -1;
    }

    /* Handle signal interrupts (like SIGINT) */
    if (PyErr_CheckSignals()) {
        return -1;
    }

    /* Recompute the remaining timeout when using nanosleep */
    if (PyTime_Monotonic(&monotonic) < 0) {
        return -1;
    }
    timeout = deadline - monotonic;
    if (timeout < 0) {
        break; // If the remaining timeout is negative, exit the loop
    }

    /* Retry nanosleep with the recomputed delay */
} while (1);

Let’s break it down layer by layer, from the code you write all the way down to the hardware.

Unraveling the Layers: From App Code to Hardware Instructions:

1.App Code : It all starts with your Python code—whether it’s time.sleep() for blocking sleep or asyncio.sleep() for non-blocking sleep. But what happens after you call one of these functions?

2.Python Implementation : When you call time.sleep(), Python internally calls a built-in implementation for sleep, in case of cPython its a C function simplified above. Based on different operating system environments, it uses different system calls.

3.System Call : Nanosleep Here’s where things get interesting. On Linux, for example, time.sleep() translates to a call either clock_nanosleep or nanosleep() system call based on what's supported[6]. This system call tells the operating system’s kernel to pause the execution of the process for the specified amount of time. But remember the "gotcha"? The kernel will try to pause for at least the time you requested, but other tasks and scheduling priorities may extend that sleep duration.

4.OS-Level Scheduling : Once the system call reaches the operating system, it enters the realm of CPU scheduling. The OS manages which tasks get CPU time and when. If other processes are running, the OS might delay waking your program slightly beyond the requested sleep duration.

5.Hardware Instructions : Finally, at the lowest level, the operating system uses hardware timers to keep track of time and manage sleep. These timers run independently of the CPU clock, which ensures that even if the system is under heavy load, the timers accurately track how long a process should sleep.

Side note - In case you saw Py_BEGIN_ALLOW_THREADS, Py_END_ALLOW_THREADS above, those are macros implemented in C to basically manage Global interpreter lock

Wrapping Up

What seems like a simple time.sleep() call actually hides a complex, multi-layered juggling to make sure it works as expected. Next time you add a sleep statement, you’ll know that much more is happening behind the scenes—making sure your program rests just the right amount.

6

References -
  1. https://github.com/python/cpython/issues/65501
  2. Benchmark for sleep
  3. Documentation for clock_nanosleep
  4. Nanosleep
  5. Sleep, async sleep examples

linked in the articles

  1. https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep

  2. https://peps.python.org/pep-0418/#time-sleep

  3. Code snippet on github

  4. https://github.com/python/cpython

  5. https://github.com/python/cpython/blob/main/Modules/timemodule.c#L2165

  6. If you found this helpful, please share it to help others find it! Feel free to connect with me on any of these platforms=> Email | LinkedIn | Resume | Github | Twitter | Instagram 💜