- ⏱️ PyQT's built-in
QTimercan drift under load, even with millisecond intervals. - đź§
time.monotonic()provides a reliable system clock immune to system time changes. - đźš« GUI event loops cause unpredictable delays, affecting timer signal precision.
- đź’ˇ Combining
QTimerwith external time tracking greatly improves timing accuracy. - ⚙️ Sub-millisecond precision in PyQT needs native code extensions or multithreading.
If you are building a PyQT application that needs precise timing—like a metronome or sensor reader—you will quickly find that PyQT timing is not very precise. Events are delayed. Callbacks drift. And then your beat lands "eventually." This article shows how accurate PyQT timers like QTimer and QTime can be. It explains what causes your timing issues. And it tells you how to lessen these issues. You can use drift correction, good event loop habits, and outside tools like time.monotonic().
Timing in GUI Applications: Why Precise Timing Is Difficult
Getting precise timing in GUI frameworks like PyQT is hard. GUIs work with event loops. These loops process callbacks, like button presses or timer ticks, in a queue. When you set up repeated actions using timers, those events also go into this queue.
But here is the problem: this queue handles everything. This includes rendering, user input, data updates, and window resizing. It all happens in the same thread unless you move tasks elsewhere. So if your app is busy, even a carefully set timer can get delayed.
For tasks that need high frequency or precision, like timed sampling, music cues, or strict animations, these delays matter a lot. It is not just about an event firing. It is about whether your program logic gets it and acts at a consistent time.
PyQT Timing Tools
PyQT has tools for basic to fairly precise timing in GUI apps. Let's look at each of them:
QTimer
- Emits a
timeout()signal at specified intervals. - Operates via the main event loop.
- Works at the millisecond level (e.g.,
QTimer.start(100)starts a 100ms interval). - You can use it for things like blinking cursors, clock updates, or checking slow data sources.
But QTimer works through the event loop, so it is not ideal for precise, real-time needs.
QTime
- It works like a stopwatch.
- People often use it to measure how long something takes.
- Returns millisecond precision using something like:
from PyQt5.QtCore import QTime
timer = QTime()
timer.start()
# Do some work
elapsed = timer.elapsed() # milliseconds
It watches passively. It helps find delays but does not control how often events happen.
QElapsedTimer
- It is like
QTime. But it is more stable for measuring elapsed time, because it uses system ticks. - It can give nanosecond results on systems that support it.
- It is very useful for checking how fast things run or for finding delays in GUI actions.
Python’s time Module (time.monotonic(), perf_counter())
time.monotonic()is good for very precise and steady interval checks.- It is not affected by clock resets or daylight saving time. It only moves forward.
time.perf_counter()is similar. But it often has even higher precision, depending on the system.
You can pair QTimer with time.monotonic() or QElapsedTimer. This lets you trigger an event. At the same time, you can track when it was supposed to fire versus when it actually did. This is very important in apps that need good performance.
How Accurate Is QTimer?
QTimer can time things down to a millisecond. But in a typical app under load, delays can be bigger and hard to predict. A 1000ms timer might fire anywhere between 1000ms and 1100ms. This depends on how many system resources are in use.
Why Does It Drift?
- Event Loop Congestion:
QTimertimers work within the event loop. So, any long task, like accessing a database or reading files, can delay when a tick is handled. - Single-Threaded UI: The UI thread handles all rendering, interaction, and timer callbacks. This is true unless you use multiple threads.
- System Delays: Things like garbage collection, CPU scheduling, or background tasks can all make timing a bit off.
Drift adds up. A 10ms delay each second becomes half a second after 50 ticks. This is not good for audio, hardware control, or data streaming apps.
Using QTime to Measure Time
The QTime module helps you measure time between events, but it does not create them. Let's say you are using a 500ms QTimer. You can track how long it actually takes between ticks:
from PyQt5.QtCore import QTime
class TimerTester:
def __init__(self):
self.clock = QTime()
self.clock.start()
def tick(self):
elapsed = self.clock.elapsed()
print(f"Elapsed: {elapsed}ms")
self.clock.restart()
This does not fix delays. But it shows you what is going on. If you keep seeing 505ms when you expect 500ms, then you know there is a problem you should fix.
Microsecond Precision: Is It Even Possible?
Simply put: no, not with only Python and PyQT.
For microsecond or better precision:
- Python is not a real-time system: Its interpreter, CPython, causes timing to be uncertain. This is due to things like GIL and garbage collection.
- Operating System overhead: Desktop operating systems are not made for real-time task scheduling. This is true unless you use a real-time operating system (RTOS) or specific kernel parts.
- Multithreading does not help much in CPython This is because of the Global Interpreter Lock, unless you use C extensions.
Still, time.perf_counter() can measure with sub-millisecond accuracy. But GUI frameworks cannot time execution at that level.
If you need nanosecond precision, for example, in astrophysics or robotics, use:
- A compiled language like C, Rust, or C++
- Linux with real-time kernel
- Dedicated microcontrollers or hardware timers that work with Python
Good Ways to Make PyQT Timers More Accurate
1. Do Not Assume QTimer Is On Time
Always use QTimer to trigger an event. But check the actual time separately.
2. Use QTimer with time.monotonic()
Python’s time.monotonic() helps track the actual time passed since a start point. This works even if the system has hiccups.
3. Fix Drift Over Time
Adjust intervals based on how much delay has built up.
Here is how to do that:
import time
from PyQt5.QtCore import QTimer
class DriftCorrectedTimer:
def __init__(self, interval_sec=0.5):
self.interval = interval_sec
self.base_time = time.monotonic()
self.tick_count = 0
self.timer = QTimer()
self.timer.timeout.connect(self.timer_event)
self.timer.start(1) # frequent checking
def timer_event(self):
now = time.monotonic()
expected_time = self.base_time + self.tick_count * self.interval
if now >= expected_time:
print(f"TICK at: {now - self.base_time:.4f}s")
self.tick_count += 1
This makes sure you do not add time based on the callback interval, which can drift. Instead, you add time based on a fixed timeline from the start.
Example: Making a Precise Metronome
Let's use all the ways mentioned above to make a basic, BPM-accurate metronome:
import sys
import time
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer
class AccurateMetronome:
def __init__(self, bpm=120):
self.interval = 60 / bpm # seconds between beats
self.base_time = time.monotonic()
self.count = 0
self.timer = QTimer()
self.timer.timeout.connect(self.tick)
self.timer.start(5) # frequent checking
def tick(self):
now = time.monotonic()
expected = self.base_time + self.count * self.interval
if now >= expected:
print(f"Beat #{self.count} at {now - self.base_time:.3f}s")
self.count += 1
app = QApplication(sys.argv)
metronome = AccurateMetronome()
sys.exit(app.exec_())
Note that:
QTimerchecks frequently, say every 5 milliseconds.- We independently calculate whether it is time for the next beat.
- This prevents drift from adding up.
Why time.monotonic() Is Better Than QTime for Elapsed Time
While QTime and QElapsedTimer are okay, Python’s time.monotonic() is:
- Unaffected by system clock changes (e.g., NTP, manual updates).
- Available on all modern operating systems, and it often uses high-resolution timers.
- The best way to replace a wall clock in your program's logic.
Example:
start = time.monotonic()
# work
elapsed = time.monotonic() - start
This is much more solid than datetime.now() or QTime.currentTime() for precise measurements.
Fixing Drift in Repeating Timers
Using QTimer.start(interval) in a simple way repeats every interval milliseconds. But any delay just adds to future intervals without being corrected.
A better way:
- Keep your base starting time.
- Always figure out the next tick from this base. Use the number of ticks multiplied by the interval.
- Use
time.monotonic()to check the actual time passed.
This keeps timing accurate, even if single ticks are delayed.
Checking Your Timer Logic
If you think your timer logic is not working as it should:
- Add logging with timestamps:
print(f"[{time.monotonic():.6f}] Tick triggered")
- Compare actual values against expected:
drift = now - expected_time
print(f"Drift: {drift*1000:.2f}ms")
- Use plotting libraries, like matplotlib, to see drift over time.
Common Mistakes Developers Make
- ❌ Assuming
QTimeris accurate — Always check. - ❌ Ignoring the event loop — Heavy GUI operations delay all callbacks.
- ❌ Not testing under load — Timing performance gets worse under stress.
- ❌ Using
time.sleep()in UI thread — This stops your interface from working.
Always test with real-world use cases if your app depends on regular timing.
More Advanced Options: C Extensions, Threads, and Other Ways
If you need accuracy under 1ms, which is rare, use a C or C++ module:
- Run the time-sensitive logic in a background thread using
QThread. - Use
pyqtSignalto send updates back to the main UI.
Or you can:
- Use
multiprocessingor other processes for true separation. But these are more complex. - Add compiled modules for critical timing tasks.
Make sure the main UI stays responsive. Do this by moving any time-sensitive tasks away from it.
Visual Timers: Keeping Your UI Together
If you are animating, think about:
QTimerat 16ms intervals, which is about 60 frames per second.- Measure time since last frame using
QElapsedTimerortime.monotonic(). - Always base animation positions, frame changes, or sync states on elapsed time. Do not base them on counted frames.
Animations that use tick count will stutter if frames are not delivered steadily. Always calculate again based on real time that has passed.
You can get high-precision timing with PyQT. But it needs careful work between tools like QTimer, QTime, and time.monotonic(). Do not treat timer events as always perfect. They are best used as signals or reminders. Your real program logic should be based on real-world clock tracking. By fixing drift using math, and by checking delays under real use, you can build apps that rely on timing and feel snappy, musical, or responsive. You will not need to rewrite them in C or switch platforms.
Citations
Mardix, S. (2024). How to get accurate timing with PyQT?. Stack Overflow. Retrieved from https://stackoverflow.com/questions/79730171
Python Software Foundation. (2023). time — Time access and conversions. Python 3.10 documentation. Retrieved from https://docs.python.org/3/library/time.html#time.monotonic