MediumQt Connection Types Explained: When connect() Does More Than You Think | by Anuj Gupta | Medium
Sitemap

Qt Connection Types Explained: When connect() Does More Than You Think

--

If you’ve written any non-trivial Qt application, you’ve typed connect(sender, &Sender::signal, receiver, &Receiver::slot) hundreds of times without a second thought. What most developers don't think about is the invisible fifth argument that decides how and when that slot actually gets called — and in multithreaded Qt applications, getting this wrong is one of the most common sources of subtle, hard-to-reproduce bugs.

That fifth argument is the connection type, and this article walks through all five of them with practical code you can drop straight into a project.

Press enter or click to view image in full size

The Signature Nobody Reads

QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot, connectionType);

connectionType defaults to Qt::AutoConnection, which is why most code never specifies it explicitly — and why most developers don't know it exists until something breaks in production, usually involving a worker thread.

Qt defines five values in the Qt::ConnectionType enum:

  • Qt::AutoConnection (the default)
  • Qt::DirectConnection
  • Qt::QueuedConnection
  • Qt::BlockingQueuedConnection
  • Qt::UniqueConnection (a modifier, not a delivery mechanism)

Let’s go through each one.

1. DirectConnection — Same Thread, Immediate Call

With Qt::DirectConnection, the slot is invoked immediately and synchronously, in the thread that emits the signal — exactly as if you'd called the slot as a regular function.

class Counter : public QObject {
Q_OBJECT
public slots:
void increment() { qDebug() << "Incremented on thread:" << QThread::currentThread(); }
};
// In main thread:
Counter counter;
QObject::connect(&someButton, &QPushButton::clicked,
&counter, &Counter::increment,
Qt::DirectConnection);

When clicked() fires, increment() runs right there, on the GUI thread, before emit returns. This is fast — no event queue overhead — but it means the emitting thread is blocked until the slot finishes. If sender and receiver live in different threads, the slot still runs on the sender's thread, not the receiver's — a common source of confusion when people assume "the object's own thread" always applies.

2. QueuedConnection — Post It to the Event Loop

With Qt::QueuedConnection, calling the signal doesn't invoke the slot directly. Instead, Qt packages up the arguments into an event and posts it to the receiver's thread event queue. The slot runs later, whenever that thread's event loop gets around to processing it.

class Worker : public QObject {
Q_OBJECT
public slots:
void processData(const QByteArray &data) {
qDebug() << "Processing on worker thread:" << QThread::currentThread();
// heavy lifting here
}
};
QThread workerThread;
Worker worker;
worker.moveToThread(&workerThread);
workerThread.start();
QObject::connect(&networkManager, &NetworkManager::dataReceived,
&worker, &Worker::processData,
Qt::QueuedConnection);

This is the standard way to safely hand work off to a worker thread. The emitting side (say, the GUI thread) doesn’t block — it fires the signal and moves on, while processData() executes asynchronously on workerThread.

Get Anuj Gupta’s stories in your inbox

Join Medium for free to get updates from this writer.

Important caveat: any custom argument types passed through a queued connection must be registered with qRegisterMetaType(), since Qt needs to copy them into the event queue.

qRegisterMetaType<MyCustomType>("MyCustomType");

Forget this, and you’ll get a runtime warning like:

QObject::connect: Cannot queue arguments of type 'MyCustomType'

3. BlockingQueuedConnection — Queued, But You Wait

Qt::BlockingQueuedConnection behaves like QueuedConnection — the slot runs on the receiver's thread — but the emitting thread blocks until the slot finishes executing.

QObject::connect(&mainThreadObject, &Controller::requestStatus,
&worker, &Worker::getStatus,
Qt::BlockingQueuedConnection);

This is useful when you need a synchronous result from a call that must execute on another thread’s context — for example, fetching a value from an object that isn’t thread-safe to touch directly.

The trap: never use this when sender and receiver are on the same thread. The emitting thread will block waiting for an event that its own event loop needs to process — a guaranteed deadlock.

// DEADLOCK if worker lives on the same thread as this call:
QObject::connect(this, &Controller::requestStatus,
worker, &Worker::getStatus,
Qt::BlockingQueuedConnection); // 💀

4. AutoConnection — Qt Decides for You

Qt::AutoConnection, the default, resolves to DirectConnection if sender and receiver live in the same thread at the time the signal is emitted, or QueuedConnection if they don't.

// No explicit type — Qt figures it out:
QObject::connect(&timer, &QTimer::timeout, this, &MainWindow::updateClock);

This is convenient, but the decision is made at emit time, based on thread affinity, not at connect time — which means if an object’s thread affinity changes later via moveToThread(), the same connection can behave differently across the life of the program. In most simple GUI apps this never matters. In apps with dynamic thread reassignment, it's worth being aware of.

5. UniqueConnection — A Modifier, Not a Type

Qt::UniqueConnection isn't a delivery mechanism at all — it's a flag you OR onto one of the above to prevent duplicate connections between the same signal and slot pair.

connect(&button, &QPushButton::clicked,
this, &MainWindow::onButtonClicked,
Qt::UniqueConnection);
// Calling connect() again with the same sender/signal/receiver/slot
// silently fails instead of creating a second connection:
connect(&button, &QPushButton::clicked,
this, &MainWindow::onButtonClicked,
Qt::UniqueConnection); // returns false, no duplicate

This is handy in code paths that might run connect() more than once — for instance, a setupConnections() function that could be called during re-initialization. You combine it with a direct or queued type: Qt::AutoConnection | Qt::UniqueConnection.

Quick Reference

TypeSlot runs onBlocks emitter?Typical useDirectConnectionSender's threadYes (synchronous call)Same-thread, low-latency signalsQueuedConnectionReceiver's threadNoCross-thread, fire-and-forgetBlockingQueuedConnectionReceiver's threadYesCross-thread, need a synchronous resultAutoConnectionDepends on thread affinity at emit timeDependsDefault — safe for most GUI codeUniqueConnection(modifier)(modifier)Prevent duplicate connections

The One Bug This Explains

If you’ve ever seen a Qt app where a slot updating a QLineEdit or QLabel from a worker thread either crashes, silently does nothing, or corrupts the UI — this is almost always a missed QueuedConnection. Qt widgets are not thread-safe, and touching them from any thread other than the GUI thread is undefined behavior, connection type or not. The fix is never "call the slot directly from the worker" — it's to emit a signal and let a properly queued connection marshal the call back onto the GUI thread where it belongs.

// Wrong: touching a widget from a worker thread
void Worker::run() {
label->setText("Done"); // 💥 undefined behavior
}
// Right: signal + queued connection back to the GUI thread
void Worker::run() {
emit resultReady("Done"); // GUI thread receives via QueuedConnection
}

Takeaway

Qt::AutoConnection gets you through 90% of everyday Qt development without ever needing to think about this. But the moment threads enter the picture — worker threads, network callbacks, background processing — knowing the difference between Direct, Queued, and BlockingQueued isn't optional. It's the difference between a responsive app and a deadlocked one, or a working feature and a crash that only shows up under load.

Tags: #Qt, #QtConnection, #QtConnectionType

--

--

Anuj Gupta
Anuj Gupta

Written by Anuj Gupta

Senior Technical Lead, 11+ years in safety-critical C++ — aviation, medical devices, industrial automation. Writes about build tools and shipping software.