Diagnosing Qt Cross-Thread Signal/Slot Delivery Failures
A diagnostic guide for Qt signals that leave a worker thread but whose slots never run, run late, or run on the wrong thread — with ordered checks and fixes tied to each finding.
10 Sept 2025, 05:46 UTC

The symptom: signals leave the worker, slots never run
You emit a signal from a worker QThread, the emit site definitely executes, and yet the slot on your GUI object never fires — or it fires in a delayed burst, or it fires but crashes inside a paint handler. This is almost never a broken connect(). It is almost always a delivery problem: the queued event carrying your signal has nowhere to go, or is being dispatched on the wrong thread.
The useful mental model: with the default Qt::AutoConnection, a signal crossing a thread boundary becomes a queued event posted to the receiver's thread. That event is only delivered if (a) the receiver lives in the thread you think it does, (b) that thread is running an event loop, and (c) every parameter type can be marshalled by the meta-object system. Failure of any one of these produces a silent or confusing symptom.
Cause and diagnostic table
| Symptom | Likely cause | Quick check |
|---|---|---|
| Slot never runs, no warnings | Receiver's thread has no running event loop (no exec()) | Log receiver->thread() and confirm that thread entered exec() |
| Slot never runs, console warning about unregistered type | Custom parameter type not registered for queued delivery | Look for "Cannot queue arguments of type 'X'" in output |
| Slot runs on the worker thread | Qt::DirectConnection forced, or receiver's affinity is the worker thread | Compare QThread::currentThreadId() at emit site and slot entry |
| Slots arrive late, in bursts | GUI thread blocked (long synchronous work), queued events can't drain | Slot executes immediately after the blocking call returns |
| Crash or glitches in painting after threading change | QWidget/QPainter touched from a non-GUI thread | Audit slot call stacks for widget access |
Ordered checks
- Confirm the connection exists. Check the boolean return of
QObject::connect()in a debug build and read the application output. Note: a successful connect only proves the signatures matched — it says nothing about whether delivery can happen. - Log thread identity at both ends. Add temporary logging at the emit site and at slot entry:
// at emit site
qDebug() << "emit from" << QThread::currentThreadId();
emit progressChanged(value);
// at slot entry
qDebug() << "slot on" << QThread::currentThreadId()
<< "receiver affinity" << this->thread();If the two current-thread ids match but you expected a cross-thread call, you have a direct connection — check for an explicit Qt::DirectConnection argument or a receiver that was moved into the worker thread by mistake.
- Verify the target thread runs an event loop. The GUI thread does by default via
QApplication::exec(). AQThreadsubclass that overridesrun()without callingexec()at the end has no event loop, so queued slots on objects living there will never dispatch. A quick probe:QTimer::singleShot(0, receiver, []{ qDebug() << "loop alive"; });— if the lambda never fires, the loop is not running. - Check parameter types. Queued connections marshal arguments through the meta-object system. Any custom type in the signature needs
Q_DECLARE_METATYPE(MyType)plusqRegisterMetaType<MyType>("MyType")before the connection is made. The runtime warning names the offending type. - Check for a blocked GUI thread. If slots arrive in bursts, something on the GUI thread is monopolizing it — a synchronous network call, a heavy loop, or
QThread::wait().
Fixes tied to findings
Wrong affinity: Move the worker object to the thread, never a QWidget. The standard pattern is a plain QObject worker plus worker->moveToThread(&thread), with the work triggered by a signal rather than by subclassing QThread::run(). Widgets must always stay in the GUI thread.
Wrong connection type: Remove explicit Qt::DirectConnection from cross-thread connects and let Qt::AutoConnection queue the call. If you need to force it, use Qt::QueuedConnection explicitly. Avoid Qt::BlockingQueuedConnection involving the GUI thread unless you have fully traced the deadlock path — if the GUI thread blocks waiting on a worker that is itself waiting on the GUI, both stop forever.
Unregistered types: Register the metatype early (e.g., in main() before any connects) and prefer passing by value or const-ref so the queued copy semantics are clear.
Blocked event loop: Move the blocking work into the worker and report progress via signals. Do not reach for QCoreApplication::processEvents() as a fix — it re-enters event handlers, can recurse into the same slot, and masks the real problem instead of solving it.
Verify the fix
Keep the thread-id logging in place until the behavior is confirmed: the slot's current thread should equal the receiver's affinity thread, and delivery should follow emission promptly under load. A stronger check: temporarily replace the real slot with a lambda that increments a thread-safe counter, then confirm the counter only advances after the target event loop starts. During development, run with fatal warnings enabled (for example QT_FATAL_WARNINGS=1 in the environment) so queued-metatype and threading warnings surface as hard failures instead of scrolling past.
When to escalate
Stop patching and redesign the threading boundary if any of these appear: direct QWidget or QPainter access outside the GUI thread; QThread::wait() called from the GUI thread; deadlocks involving mutexes combined with queued calls; or crashes in paint/event handling that began after a threading change. These indicate the ownership and affinity model is wrong, not a single connection.
Version note: the Auto/Direct/Queued semantics and metatype-based marshalling described here are stable across Qt 5 and Qt 6, but registration helpers and type strictness differ between minor versions. Confirm the exact behavior against the Qt version your project ships, and treat the diagnostics above as a starting point to verify in your own build.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.