PyQt thread execution timeout in the background

Why time.sleep() takes longer than expected when your PyQt app is minimized on macOS, and how to fix it
Heads up! You've already completed this tutorial.

When I minimize my PyQt application while a background thread is running, time.sleep() calls take much longer than expected. For example, a time.sleep(1) might take several seconds. When I bring the window back to the foreground, everything returns to normal. What's causing this, and how can I fix it?

This is a common issue on macOS that can be confusing when you first encounter it. This isn't a problem with your code, the macOS operating system is deliberately slowing them down. Let's look at what's happening and what you can do about it.

Why time.sleep() takes longer when minimized

The time.sleep() function doesn't guarantee exact timing. According to the Python documentation:

The suspension time may be longer than requested by an arbitrary amount because of the scheduling of other activity in the system.

When you minimize your application, the OS may reduce the priority of your app's threads. On macOS specifically, a feature called App Nap kicks in. App Nap is designed to save energy by throttling applications that aren't visible on screen. This means your background threads get far less CPU time, and calls like time.sleep(1) can stretch out to many seconds.

On Windows, this behavior generally doesn't occur — threads continue running at their normal priority even when the window is minimized.

Setting thread priority with QThread

One approach is to raise the priority of your QThread so the OS is less likely to throttle it. You can do this with setPriority():

python
self._thread.start()
self._thread.setPriority(QThread.Priority.HighestPriority)

Note that setPriority() must be called after start(), since the thread needs to be running before you can change its priority.

Here are the available priority levels in PyQt6:

Priority Value
QThread.Priority.IdlePriority 0
QThread.Priority.LowestPriority 1
QThread.Priority.LowPriority 2
QThread.Priority.NormalPriority 3
QThread.Priority.HighPriority 4
QThread.Priority.HighestPriority 5
QThread.Priority.TimeCriticalPriority 6
QThread.Priority.InheritPriority 7

However, there's a catch. As the Qt documentation notes:

The effect of the priority parameter is dependent on the operating system's scheduling policy. In particular, the priority will be ignored on systems that do not support thread priorities.

On macOS, thread priorities may have limited or no effect when App Nap is active. So while setting priority is good practice, it won't necessarily solve the problem on its own.

Disabling App Nap on macOS

The most reliable fix on macOS is to disable App Nap. You can do this system-wide by running this command in your terminal:

sh
defaults write NSGlobalDomain NSAppSleepDisabled -bool YES

This tells macOS not to throttle any application when it's in the background. After running this command, your background threads should execute with normal timing even when the window is minimized.

To re-enable App Nap later:

sh
defaults write NSGlobalDomain NSAppSleepDisabled -bool NO

If you'd prefer to disable App Nap only for a specific application, you can target it by bundle identifier:

sh
defaults write com.yourcompany.yourapp NSAppSleepDisabled -bool YES

Disabling App Nap programmatically

For a more self-contained solution, you can disable App Nap from within your Python application using the subprocess module:

python
import subprocess
import sys

if sys.platform == "darwin":
    subprocess.run(
        ["defaults", "write", "NSGlobalDomain", "NSAppSleepDisabled", "-bool", "YES"]
    )

You could also use the pyobjc library to interact with macOS APIs directly and disable App Nap for just your process, though the terminal command is simpler for most cases.

Consider alternatives to time.sleep() for simulating I/O

If you're using time.sleep() to simulate I/O operations (like network requests), keep in mind that time.sleep() behaves differently from actual I/O in important ways. A sleep call tells the OS "I have nothing to do for X seconds," which makes it a prime candidate for throttling. Real I/O operations — network calls, file reads, database queries — are waiting for external data, and the OS handles their scheduling differently.

For simulating slow network I/O, you could use an actual HTTP request to a service like httpbin which lets you specify a response delay:

python
import urllib.request

def simulate_slow_io(delay_seconds):
    url = f"https://httpbin.org/delay/{delay_seconds}"
    urllib.request.urlopen(url)

This gives you a more realistic simulation of I/O behavior and is less likely to be affected by App Nap, since the thread is genuinely waiting on network activity rather than explicitly sleeping.

Using QTimer instead of time.sleep()

If your goal is to run something periodically rather than simulate I/O, consider using QTimer instead of sleeping in a loop. QTimer is integrated with Qt's event loop and tends to be more reliable:

python
from PyQt6.QtCore import QTimer

timer = QTimer()
timer.setInterval(1000)  # 1 second
timer.timeout.connect(do_something)
timer.start()

QTimer runs on the main thread's event loop, so it's a different approach from running a loop in a QThread, but for periodic tasks it's often a better fit.

Complete working example

Here's the original example updated for PyQt6, with thread priority set and a note about App Nap:

python
import sys
import random
import time
import functools

from PyQt6.QtWidgets import (
    QApplication, QWidget, QPushButton, QMainWindow, QHBoxLayout
)
from PyQt6.QtCore import QThread, QObject


def clock(func):
    """Decorator that prints how long a function call took."""
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_str = ", ".join(repr(arg) for arg in args) if args else ""
        print(f"[{elapsed:0.5f}s] {name}({arg_str})")
        return result
    return clocked


@clock
def go_sleep(sleep_time):
    time.sleep(sleep_time)


def go_run():
    for i in range(100):
        go_sleep(random.randint(1, 3))


class WorkThread(QObject):
    def __init__(self):
        super().__init__()

    def run(self):
        go_run()


class WinForm(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Thread Timeout Test")
        self.button1 = QPushButton("Run")
        self.button1.clicked.connect(self.on_button_click)

        self._thread = QThread(self)
        self.wt = WorkThread()
        self.wt.moveToThread(self._thread)

        layout = QHBoxLayout()
        layout.addWidget(self.button1)
        main_frame = QWidget()
        main_frame.setLayout(layout)
        self.setCentralWidget(main_frame)

    def on_button_click(self):
        self.button1.setText("Running")
        self._thread.started.connect(self.wt.run)
        self._thread.start()
        # Set high priority after starting the thread.
        self._thread.setPriority(QThread.Priority.HighestPriority)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    form = WinForm()
    form.show()
    sys.exit(app.exec())

Try running this example, then minimize the window. Watch the console output — if you're on macOS without App Nap disabled, you'll likely see the sleep times stretching beyond what was requested. After disabling App Nap with the terminal command shown earlier, run it again to confirm the timing returns to normal.

Summary

  • time.sleep() is allowed to sleep longer than requested. This is by design in Python and the OS.
  • On macOS, App Nap aggressively throttles minimized applications, making this much worse.
  • Setting QThread priority with setPriority() can help on some operating systems but may not be enough on macOS.
  • The most effective macOS fix is to disable App Nap via defaults write NSGlobalDomain NSAppSleepDisabled -bool YES.
  • For simulating I/O, prefer actual network calls over time.sleep(), as the OS treats them differently.
  • This issue does not typically occur on Windows.
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Bring Your PyQt/PySide Application to Market

Specialized launch support for scientific and engineering software built using Python & Qt.

Find out More

Martin Fitzpatrick

PyQt thread execution timeout in the background was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.