Updating Qt Widgets Safely from Worker Threads
Use Qt::QueuedConnection to update widgets from worker threads without crashing the GUI.
05 Sept 2026, 11:44 UTC

A worker thread should not touch QLabel
A calculation finishes on a background thread, and the natural impulse is to update the UI immediately: label->setText("done"). In Qt, widget classes such as QLabel, QPushButton, and QProgressBar are not thread-safe. Only the thread that owns the QApplication instance—normally the main thread—should call widget methods. Direct calls from a worker can corrupt internal state, race with the event loop, or crash outright.
The useful takeaway is simple: never call widget methods from a worker thread. Instead, let the worker announce an event, and let Qt deliver that announcement to the GUI thread.
Queued connections marshal the call across threads
A signal is a message a Qt object can emit. A slot is a function invoked in response. When you connect a signal to a slot with Qt::QueuedConnection, Qt does not call the slot immediately. It copies the arguments into an event and posts that event to the receiver's event loop—the loop that processes window messages, timers, and posted calls. When the receiver's thread runs that loop, it dispatches the event and executes the slot there.
This mechanism depends on thread affinity: each QObject belongs to the thread where it was created or last moved. The queued connection automatically respects that boundary, so the slot runs in the receiver's thread without explicit locks.
Worked example: a counter worker and a label
The following example assumes Qt 6 and a build system that runs the Qt Meta-Object Compiler (moc) on the source file. A Worker object lives on a background thread and emits dataReady(int). A MainWindow object lives in the main thread and connects to that signal with Qt::QueuedConnection.
#include <QApplication>
#include <QLabel>
#include <QObject>
#include <QThread>
#include <QTimer>
#include <QDebug>
class Worker : public QObject {
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent) {}
public slots:
void doWork() {
for (int i = 0; i < 5; ++i) {
QThread::msleep(500);
emit dataReady(i);
}
emit finished();
}
signals:
void dataReady(int value);
void finished();
};
class MainWindow : public QObject {
Q_OBJECT
public:
explicit MainWindow(QObject *parent = nullptr) : QObject(parent) {
label = new QLabel("waiting...");
label->show();
}
public slots:
void updateLabel(int value) {
qDebug() << "slot running on thread" << QThread::currentThreadId();
label->setText(QString::number(value));
}
private:
QLabel *label;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
QThread workerThread;
Worker worker;
worker.moveToThread(&workerThread);
QObject::connect(&worker, &Worker::dataReady,
&window, &MainWindow::updateLabel,
Qt::QueuedConnection);
QObject::connect(&worker, &Worker::finished,
&workerThread, &QThread::quit,
Qt::QueuedConnection);
QObject::connect(&workerThread, &QThread::finished,
&app, &QApplication::quit,
Qt::QueuedConnection);
QTimer::singleShot(0, &worker, &Worker::doWork);
workerThread.start();
return app.exec();
}
#include "main.moc"
Where to run: compile and run this as a normal user on a development machine; no administrator privileges are required. Build with Qt 6 (the same connection type exists in Qt 5).
Expected behavior: the label changes every half second, and the debug line shows the slot executing on the main thread's ID, not the worker's. If you remove Qt::QueuedConnection and rely on the default Qt::AutoConnection, Qt still chooses a queued connection because the sender and receiver live in different threads, but being explicit makes the intent clear to anyone reading the code.
Trade-offs and limitations
- Event overhead. Every emission posts an event. This is fine for occasional progress or completion notices, but it is not suitable for high-frequency data such as a 60 Hz sensor stream.
- Event loop required. The receiver's thread must run an event loop. The main thread does via
app.exec(). If you move the receiver to a plainQThreadwithout starting its loop or callingexec(), queued signals are silently dropped. - Keep slots lightweight. If
updateLabelperformed heavy computation, it would stall the main event loop and freeze the window. - Object lifetime. Ensure the receiver outlives the connection, or use
QPointer/ check validity, because a queued event delivered to a destroyed object is unsafe.
Practical verification
- Build the example with Qt 6 and run it.
- Observe that the label text changes without the window becoming unresponsive.
- Confirm the debug output shows the slot running on the main thread ID.
- To test the event-loop requirement, temporarily move
MainWindowto a worker thread without an event loop; the label should stop updating, demonstrating that queued delivery depends on that loop.
Bottom line
Use Qt::QueuedConnection when a background thread needs to cause a UI change. It is the standard, lock-free way to keep widget access on the thread that owns them. Measure your update frequency first; if events are extremely frequent, consider batching updates or using a custom event instead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.