qb follows Semantic Versioning 2.0.0. The version is defined in
cmake/qbConfig.cmake (QB_FRAMEWORK_VERSION) and consumed by project() in CMakeLists.txt — that
file is the single source of truth, and every other version string in the tree is a copy of it.
main and develop both report 3.0.0, and no v3.0.0 tag exists yet — so a build from either
branch advertises a version that is not yet tagged. The latest tagged release remains 2.6.0.
main was fast-forwarded to develop on 2026-08-11 in preparation for the release; until the tag
lands, the default clone is a pre-release 3.0.0. That release is a major one — see CHANGELOG.md for what makes it major.
The qbm modules (qbm-http, qbm-pgsql, qbm-redis) carry the framework version rather than
versions of their own. They are not standalone-configurable, so a module version can only ever mean
"the qb this was built against"; project(qbm-<name> VERSION ...) is kept in lockstep with
QB_FRAMEWORK_VERSION, and each module's scripts/doc-lint.sh fails if the two disagree whenever
both trees are visible.
Given a version MAJOR.MINOR.PATCH:
- PATCH (
2.0.x) — bug fixes, security fixes, documentation, and internal improvements. No source-breaking API changes. - MINOR (
2.x.0) — new, backward-compatible features. Existing code that compiles against2.0keeps compiling against2.x. New deprecations may be introduced (see below) but nothing already deprecated is removed. - MAJOR (
x.0.0) — changes that may require source edits, including removals of previously deprecated APIs.
Guaranteed within a major version, as described above. Code that compiles against 3.0 keeps
compiling against every later 3.x; anything that would require a source edit waits for 4.0.
The installed CMake package version file is generated with COMPATIBILITY SameMajorVersion, so
find_package(qb 3.0) accepts any installed 3.x and rejects a different major. That check is
about source: it answers "is this qb new enough to compile my code?". It is not, and cannot be,
a statement about binaries — see the next section, and note in particular that a find_package
that succeeds can still be followed by a link failure.
qb promises no binary compatibility between any two versions, in either direction, including
between 3.0.0 and 3.0.1. Every object file in a program — yours, qb's, and any static library
of your own that exposes a qb type — must be compiled against the same qb it links against.
Upgrading qb means rebuilding all of it. There is no drop-in replacement.
That is stronger than the usual "ABI stability is not guaranteed" disclaimer, and it is deliberate:
qb would rather be checked than trusted. qb's own version is an axis of a link-time fingerprint,
so the promise is enforced rather than merely documented. A consumer compiled against 3.0.0
headers and linked against a 3.0.1 archive does not run and misbehave; it fails to link, naming
the version it was compiled with.
Everything except the source API. A patch release may change the layout of any type, the body of
any inline function or template, the size of any private member, and the order or identity of
anything not part of the documented public API. qb is a heavily templated, CRTP-based, partly
header-only framework: most of what a consumer compiles is qb's headers, so most changes are
layout-visible by construction. Several build options additionally change code generation on their
own — QB_ENABLE_NATIVE_ARCH, QB_ENABLE_LTO, QB_ENABLE_FAST_MATH, and any sanitizer.
src/qb/utility/abi.h gives the archive one symbol per axis, named after the value it was
compiled with, and makes every consumer translation unit that parses a qb header reference one
symbol per axis named after the value it is being compiled with. Same configuration, the
references resolve; different configuration, the link fails and the undefined symbol names the axis.
There is no opt-out macro: each axis is a configuration in which the two sides are provably unsound
together, so the remedy is to rebuild qb, not to silence the check.
An axis qualifies only if a mismatch is simultaneously possible in a build that otherwise compiles, silent under every tool, and unsound. Five qualify:
| Axis | Symbol | What differs when it is violated |
|---|---|---|
| qb version | qb_abi_version_M_m_p |
anything (see above) — and it is the axis that catches a consumer compiled with a hand-written -I/-l line instead of qb's CMake usage requirements |
| cache line | qb_abi_cacheline_N |
KNOWN_L1_CACHE_LINE_SIZE re-lays out qb::Event and the coroutine frame pool |
| exceptions | qb_abi_exceptions_[01] |
-fno-exceptions forks inline bodies the archive also defines |
| coroutine debug | qb_abi_coroutine_debug_[01] |
QB_DEBUG_COROUTINES grows task<T>::promise_type |
| jthread source | qb_abi_std_jthread_[01] |
a standard library without __cpp_lib_jthread, or QB_COMPAT_FORCE_THREAD_FALLBACK, resizes qb::jthread and moves every member after it in qb::Main and qb::VirtualCore |
Measured against an installed 3.0.0 prefix (macOS 26 / arm64, AppleClang 21), one axis moved per
row, everything else held equal:
consumer configuration result
----------------------------------------- -------------------------------------------------------
3.0.0, cacheline 64, coro-debug off LINK OK <- control
qb 3.0.1 (patch bump) undefined: _qb_abi_version_3_0_1
qb 3.1.0 (minor bump) undefined: _qb_abi_version_3_1_0
qb 4.0.0 (major bump) undefined: _qb_abi_version_4_0_0
hand-written -I/-l, no usage requirements undefined: _qb_abi_version_unknown__compile_with_qb_s_…
-DQB_DEBUG_COROUTINES undefined: _qb_abi_coroutine_debug_1
-DKNOWN_L1_CACHE_LINE_SIZE=128 undefined: _qb_abi_cacheline_128
-DQB_COMPAT_FORCE_THREAD_FALLBACK undefined: _qb_abi_std_jthread_0
-fno-exceptions undefined: _qb_abi_exceptions_0
-DNDEBUG LINK OK (not an axis — see below)
-std=c++23 LINK OK (not an axis — layout is identical)
On MSVC and clang-cl the same axes are additionally emitted as #pragma detect_mismatch records,
which the Microsoft linker reports as LNK2038 naming both values. That pragma is an inert
no-op on Mach-O and ELF, so it is a bonus where it works and never the mechanism.
It is a detector for five specific silent misconfigurations, not a general ABI checker. It compares qb's configuration against qb's configuration. It cannot see, and does not claim to see:
- The compiler, or its version. See the next section — this is the one with a worked example.
- The standard library. libstdc++ against libc++, or two libstdc++ versions, are ABI axes the standard libraries arbitrate with their own tags; qb does not duplicate that.
- Standard-library hardening or debug modes (
_GLIBCXX_DEBUG,_LIBCPP_HARDENING_MODE, …). NDEBUG— deliberately excluded, and that exclusion is qb's obligation rather than yours. A consumer'sNDEBUGcomes from its ownCMAKE_BUILD_TYPE, including the unset default, which defines none; a Debug consumer against a Release qb is a supported and CI-tested configuration, so fingerprintingNDEBUGwould turn the default consumer configuration into a hard link failure. What qb owes in exchange is that no public type, member declaration or alignment in a shipped header is selected byNDEBUG(or_DEBUG/DEBUG) — the disagreement would be invisible to the linker and tofind_package, surfacing as silent memory corruption. Two such splits existed and were removed in 3.0 (qb::unordered_map/unordered_set, andqb::Event::id_type);scripts/check-abi-macro-split.pyfails CI on a new one. One residual is open and stated rather than papered over:assert()sites insideinlineand template bodies in shipped headers mean two translation units that disagree aboutNDEBUGemit two bodies under one vague-linkage symbol, and object order alone decides which survives — so a mismatched consumer can silently lose an assertion. Do not mixNDEBUGacross translation units; the building guide says so where a user will hit it.- Optimization, LTO,
-march, or sanitizers. Matching those is your build system's job. - Feature switches (
QB_HAS_SSL,QB_HAS_QUIC,QB_HAS_COMPRESSION,QB_WITH_LOGGING). These gate whole types rather than members of a type that exists either way, so a mismatch is a compile error, not silent corruption — and the genuinely silent case, a hand-written-I/-lconsumer, is caught by the version axis.
The compiler is part of the "same configuration" requirement, not just the standard library, and the
fingerprint cannot check it. The worked example is real and is a 3.0 fix rather than a hypothetical:
clang appends the cxx11 ABI tag (B5cxx11) to the mangled name of a function-local static whose
type carries it, and gcc does not. A local static that the code treats as one shared object — a
sentinel compared by address, a cache whose identity matters — then exists twice in a mixed binary,
with no link-time diagnostic at all. In qb's case an empty qb::unordered_map created on one side
and destroyed on the other freed static storage (see Fixed in CHANGELOG.md).
qb's own headers are clean of that class and the ubuntu-abi-sentinel-sweep CI job keeps them that
way — it compiles a probe with both compilers and diffs symbol names, and carries a negative control
so a green result means "the two compilers agree" rather than "the probe stopped instantiating".
The guarantee stops at qb's surface. Your own code and your other dependencies are not swept.
Build the whole program with one compiler.
Note also that macOS is structurally blind to this class: libc++ has no cxx11 tag, so a macOS-only workflow cannot reproduce it however many configurations it tries.
The installed prefix contains static archives and nothing else — libqb-core.a, libqb-io.a,
libqev.a, and one .a per qbm module. There is no shared build to consume: SOVERSION is set
exactly once in the whole tree and it is on the vendored event-loop fork, never on qb's own targets;
QB_BUILD_SHARED_LIBS exists but BUILD_SHARED_LIBS appears in no CI workflow, so a shared qb has
never been configured anywhere. Treat it as unsupported for the 3.0 series. What that implies:
- Upgrading qb is a rebuild of your program, not a swap of a file. This is the practical form of the no-binary-compatibility promise, and it is why the version axis is not an inconvenience: with static linkage there is no scenario in which replacing qb without recompiling was going to work.
- No parallel installs. Without
SOVERSIONthere is no distro-style side-by-side of two qb versions in one prefix. - A few qb entities must exist exactly once per process — the event type-id counter, the
ServiceActorregistry, the per-thread event loop andVirtualCore, the coroutine frame pool, and a null-object sentinel compared by address. They are vague-linkage entities that the dynamic linker coalesces across images, which-fvisibility=hiddenwould otherwise switch off; qb annotates themvisibility("default")so a host executable plus adlopened plugin that both statically link qb still share one of each. Without that annotation two event types silently receive the same id and the router sends them to the same slot. The annotation is empty on Windows, where a shared qb needs export-macro work first.
-
The link error already names the axis and the value your translation unit was compiled with — for example
_qb_abi_cacheline_128,referenced from: qb::detail::abi_fingerprint in main.o. -
The archive's side of the story needs no demangler and no qb source tree:
nm -g <prefix>/lib/libqb-io.a | grep qb_abi strings <prefix>/lib/libqb-io.a | grep '^qb-abi '
-
A prebuilt prefix also records its configuration as a file —
share/qb/abi-fingerprint.txt, read back out of the archive rather than re-derived, so it cannot drift from the artefact. Comparing two prefixes is adiff. See INSTALL.md. -
Consume qb through
find_packageoradd_subdirectory. A hand-written-I/-lline misses qb's usage requirements — not only the version, but the feature macros — and yields theqb_abi_version_unknown__compile_with_qb_s_cmake_usage_requirementssymbol by design.
When an API is superseded:
- It is marked deprecated and documented in the CHANGELOG under Deprecated, with the replacement.
- It keeps working for the remainder of the current major series.
- It may be removed only in the next major release, listed under Removed.
A migration path is provided for every removal. See, for example, the time-type change described in the migration guide.
One series is maintained at a time: the latest minor of the current major. When 3.0.0 is
released it becomes that series, and 2.6.x stops receiving releases — including for fixes that
already exist on develop. There is no backport commitment to the previous major and no separate
long-term-support branch; this section will be updated if one is introduced.
That is a deliberate policy rather than an oversight, and it has a visible consequence worth stating
plainly: a fix merged after 2.6.0 was tagged is reachable only by moving to the next release, and
for 3.0.0 that means a major upgrade with the source-breaking changes listed under Removed in
CHANGELOG.md. Security fixes are the one place this could bite hardest, so
SECURITY.md states the same policy in the same words rather than implying a
broader guarantee.
qb requires C++20 by default (QB_CXX_STANDARD=20, extensions off) and also supports
QB_CXX_STANDARD=23 for newer standard-library implementations. CMake 3.24 is the minimum. Supported
compilers and platforms are listed in INSTALL.md. A change to the minimum required
standard or toolchain is treated as a breaking change and reserved for a major release.