Diagnosing Lost Qt Signal‑Slot Connections Across Threads
When a Qt signal emitted from a worker thread never reaches a GUI slot, the culprit is often a mis‑configured connection, missing event loop, or premature object deletion. This guide walks through a diagnostic flow, checks, and fixes to restore reliable cross‑thread signal‑slot delivery.
17 May 2026, 02:40 UTC

Problem: A signal emitted from a worker thread never arrives at its GUI slot
When using Qt’s thread‑safe signal‑slot mechanism, a common pitfall is that a signal emitted from a worker thread appears to “vanish” when the slot resides in a different thread. The symptom is simple: the GUI never updates, the slot is never called, and the application continues to run.
Takeaway
Ensure the connection type is Qt::QueuedConnection, the receiver outlives the queued event, and both objects have active event loops. Use QT_LOGGING_RULES and explicit thread‑affinity checks to surface hidden issues.
Diagnostic Table
| Condition | Likely Cause | Diagnostic Check | Fix |
|---|---|---|---|
| Slot never called after emit | Connection is DirectConnection across threads | Check the connection type in logs or QMetaObject::Connection::type() | Reconnect with Qt::QueuedConnection |
| Receiver deleted before event processes | Object destroyed while queued event pending | Verify receiver->thread() and object lifetime; use QPointer | Keep receiver alive until event loop drains; use QPointer or std::shared_ptr |
| Event loop not running in receiver thread | Thread has no QThread::exec() active | Call QThread::isRunning() and QThread::eventDispatcher() | Start the event loop with QThread::exec() or QCoreApplication::exec() |
| Connection established after moving receiver | Connection bound to old thread affinity | Check receiver->thread() before and after moveToThread() | Move receiver before connect() |
| Slot fires multiple times | Duplicate connections | Search for repeated connect() calls | Use Qt::UniqueConnection or disconnect before reconnecting |
| Connection silently fails on Windows DLL | Missing __declspec(dllexport) for QObject class | Compile with appropriate export macros; inspect linker output | Add export macro or adjust DLL build settings |
Ordered Checks
- Verify Connection Type
In a debug build, assert the connection type:
If the assertion fails, the connection was not established or used a different type.Q_ASSERT(QMetaObject::connect(sender, &Sender::dataReady, receiver, &Receiver::onDataReady, Qt::QueuedConnection)); - Check Thread Affinity
Print the threads involved:
Both objects should have distinctqDebug() << "Sender thread:" << sender->thread(); qDebug() << "Receiver thread:" << receiver->thread(); qDebug() << "Current thread:" << QThread::currentThread();QThreadinstances when usingQt::QueuedConnection. - Ensure Receiver Lifetime
Wrap the receiver in a
QPointerif it may be deleted before the queued event executes:
This guard prevents a use‑after‑free.QPointer<Receiver> ptr(receiver); QObject::connect(sender, &Sender::dataReady, [ptr](const QJsonObject &obj){ if(ptr) ptr->onDataReady(obj); }); - Confirm Event Loop Running
In the receiver’s thread, verify that an event loop is active:
IfQThread *t = receiver->thread(); qDebug() << "Event loop running:" << t->isRunning();false, start it witht->exec()or move the receiver to a thread that already runsQCoreApplication::exec(). - Move Receiver Before Connect
Always call
receiver->moveToThread(targetThread)before establishing the connection. After moving, reconnect if necessary. - Enable Connection Logging
Set the environment variable before launching the application:
Qt will emit messages likeexport QT_LOGGING_RULES='qt.qobject.connect=true;qt.thread.queuedconnection=true'QObject::connect: queued connection to slot on thread X, helping to verify the path. - Stress‑Test with Rapid Emissions
Emit thousands of signals in quick succession and monitor pending events:
If events drop, the event loop is overloaded or the connection is mis‑configured.for(int i=0;i<10000;i++) sender->dataReady({{"i",i}}); QAbstractEventDispatcher::instance()->processEvents(); qDebug() << "Pending events:" << QAbstractEventDispatcher::instance()->pendingEvents();
Concrete Example
Below is a minimal, compilable example illustrating a correct setup. The worker thread emits dataReady; the GUI slot receives it via a queued connection.
#include <QtCore>
class Worker : public QObject {
Q_OBJECT
public:
Q_INVOKABLE void start() {
for(int i=0;i<5;i++) {
emit dataReady({{"value",i}});
QThread::sleep(1);
}
}
signals:
void dataReady(const QJsonObject &);
};
class Gui : public QObject {
Q_OBJECT
public slots:
void onDataReady(const QJsonObject &obj) {
qDebug() << "Slot executed in thread" << QThread::currentThread();
qDebug() << "Received:" << obj;
}
};
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
QThread workerThread;
Worker worker;
worker.moveToThread(&workerThread);
Gui gui;
// Connect with QueuedConnection explicitly
Q_ASSERT(QMetaObject::connect(&worker, &Worker::dataReady,
&gui, &Gui::onDataReady,
Qt::QueuedConnection));
QObject::connect(&workerThread, &QThread::started, &worker, &Worker::start);
workerThread.start();
return app.exec();
}
Running the program should print the slot execution thread and the received JSON object, confirming that the queued event traversed the thread boundary.
Escalation Criteria
- If after 5 seconds the slot has never fired, verify that the worker thread’s event loop is active.
- If the slot fires but receives stale data, check that the worker emits a fresh object each time.
- If the application crashes on slot execution, inspect the stack trace for a use‑after‑free; the receiver may have been destroyed.
Limitations & Practical Checks
- Qt’s
AutoConnectionwill default toDirectConnectionif the sender and receiver share the same thread at emit time; always useQueuedConnectionwhen crossing threads. - On Windows, QObject classes exported from DLLs require the correct
__declspec(dllexport)macro; missing exports lead to silent failures. - Event loops must be running; if you embed Qt in an existing non‑Qt event loop, use
QEventDispatcherintegration. - Large volumes of queued events can overwhelm the event dispatcher; consider batching or throttling emissions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.