A guitarist opens a set. The laptop on stage runs a DAW session with your plugin on the master bus. Somewhere in the second song, the venue’s Wi-Fi hiccups — and the audio drops out for half a second. Not because the DSP failed, not because the CPU spiked. Because a license check inside the plugin decided to phone home from the audio callback, and the network made it wait.
The support ticket that follows will say “your plugin glitches.” Nobody writes “your licensing code blocked my audio thread” — the user can’t see that, and often the developer can’t either, because the check passes instantly on the office network where it was tested. This failure mode ships silently and detonates in the field, in exactly the situations where audio software must not fail.
Real-time safety is well-trodden ground in audio development. Licensing is well-trodden ground in business-of-software writing. The intersection — where the license check actually runs, and where its result actually lives — is oddly underdocumented, and it’s where this particular bug breeds. This guide covers both halves: keeping licensing off the audio thread, and then the subtler question that follows — where the cached verdict belongs in a process you don’t own.
What the Audio Thread Will Not Forgive
The audio callback runs under a deadline that most code never faces. At a 128-sample buffer and 48 kHz, the host calls your processBlock roughly every 2.7 milliseconds, and your code — along with every other plugin in the session — must finish before the next call. Miss the deadline and the result is not a slow spinner or a dropped frame. It’s an audible click, a dropout, in the worst case a squeal through a PA system — and on a festival-sized rig, a full-scale squeal is not an annoyance but a hearing-safety incident for everyone standing in front of the speakers.
There’s a sobering backdrop to this deadline: the operating system underneath makes no promise to honor it. macOS and Windows are not real-time operating systems — no scheduler contract guarantees that your callback runs to completion in time, every time. Desktop audio works because the OS offers high-priority audio threads and because decades of accumulated practice have taught the industry what it can get away with — trust built on experience, not on guarantees. Where a missed deadline costs more than a dropout — engine controllers in cars, flight software in aircraft — nobody accepts that kind of trust: those systems run real-time operating systems with provable scheduling. Audio on a desktop OS lives in an uncomfortable middle: real-time expectations on a best-effort OS. Which is exactly why the code inside the callback must be beyond reproach — the margin the OS doesn’t guarantee is margin your code cannot afford to waste.
The discipline that follows is strict: nothing with unbounded latency may run on that thread. Four categories cover most sins:
- Network I/O. A license validation request is DNS resolution plus a TLS handshake plus an HTTP round trip. On a good day that’s tens of milliseconds — already ruinous. On a bad day it’s a 30-second timeout.
- File I/O. Reading a license file from disk feels harmless until the OS decides the disk is busy, the file is on a network share, or an antivirus scanner wants a look first.
- Locks. Any mutex shared with a non-real-time thread invites priority inversion: the audio thread waits on a lock held by a thread the scheduler has parked.
- Heap allocation.
malloctakes a lock internally on most platforms. Building a request string, parsing a response, constructing astd::string— all of it allocates.
Now look at what a typical isLicensed() call does: builds a request (allocation), reads a serial from disk (file I/O), sends it to a server (network), and coordinates the answer (locks). It commits all four sins in one line of code. Even a purely offline check — read the license file, decrypt, verify — still touches the disk and the allocator.
So the rule is not “keep license checks fast.” The rule is: no licensing code on the audio thread, ever. Not the online check, not the offline check, not the “it’s just reading one file” check.
The Shape of a Safe Integration
The correct architecture is old, boring, and reliable — which is exactly what you want near a real-time deadline:
- Validate away from the audio thread. At instance construction, on the message thread, or on a background thread you own. This code is allowed to take three seconds; nobody’s audio depends on it.
- Cache the verdict. The result of validation is a small value: licensed or not (perhaps with a shade of “trial,” “expired,” “grace period”).
- Let the audio thread read a lock-free flag. A
std::atomic<bool>(or astd::atomic<int>over an enum) costs nanoseconds to read and can never block. - Re-validate in the background if your model calls for periodic checks — same rule, same thread discipline, same atomic hand-off of the result.
In JUCE terms, the minimal shape looks like this:
class MyPluginProcessor : public juce::AudioProcessor
{
public:
MyPluginProcessor()
{
licenseChecker = std::thread ([this]
{
// This may take seconds. Out here, that's fine.
const bool ok = runLicenseCheck();
licensed.store (ok, std::memory_order_release);
});
}
~MyPluginProcessor() override
{
// Never let the thread outlive the instance (more on this below).
if (licenseChecker.joinable())
licenseChecker.join();
}
void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override
{
if (! licensed.load (std::memory_order_acquire))
{
buffer.clear(); // degrade gracefully — no I/O, no locks, no UI calls
return;
}
// ... actual DSP ...
}
private:
std::atomic<bool> licensed { false };
std::thread licenseChecker;
};
Two honest footnotes on this sketch. First, the destructor join() is the simplest correct shape, but if runLicenseCheck() can sit in a 30-second network timeout, closing the plugin will sit with it — a production integration wants a cancellable request or a bounded timeout, so teardown stays snappy. Neither is exotic: HTTP libraries accept a hard per-request timeout, and libcurl’s progress callback can poll an std::atomic<bool> abort flag — return non-zero from it and the transfer stops immediately. Second, whether licensed starts as false (silence until proven licensed) or true (benefit of the doubt until proven otherwise) is a product decision, not a technical one. Starting pessimistic punishes a paying customer on a flaky connection; starting optimistic gives a cracked copy a few free seconds. Most vendors who think about it choose to be kind to the paying customer and enforce on the result, not on the wait.
What the flag must never do is gate the audio abruptly and rudely. If validation fails, fade to bypass, show a message in the editor, give the user a path to fix it. The audio thread’s only job in this story is to read one atomic and carry on.
One objection deserves a direct answer before we move on: doesn’t a single cached flag make cracking easier — patch one branch and the plugin runs free? Yes, a single verdict is a single patch point — and no, that’s not an argument against the pattern. A cracker with a disassembler will find your license branch whether it reads an atomic or calls a function; scattering checks through the callback wouldn’t hide it, it would only add dropout risk for the customers who paid. This is a real-time-correctness guide, not an anti-tamper guide. Resistance to patching lives in different layers — signed license files, server-side validation, binary integrity checks — and none of those belong on the audio thread either. Design the threading for your honest users; build the tamper resistance elsewhere.
Before std::atomic: asking the compiler nicely
If you learned C++ before 2011, you may remember doing this with volatile bool licensed; — and it’s worth being precise about why that era is over. volatile promises one thing only: the compiler won’t optimize the variable’s reads and writes away or cache the value in a register. It promises nothing about atomicity or about the ordering of surrounding operations between threads. On strongly-ordered x86 hardware it mostly worked by accident, and MSVC for years quietly gave volatile acquire/release semantics — which taught a generation of Windows developers a lesson that was never portable. The same era gave us register, the other “please, compiler” keyword: a hint to keep a variable in a CPU register, with no guarantee it would — deprecated in C++11, removed entirely in C++17. C++11’s std::atomic replaced the polite requests with a contract: defined atomicity, defined memory ordering, on every platform. If you meet a volatile bool guarding thread state in a plugin codebase today, you’ve found a fossil. It doesn’t need carbon dating; it needs replacing.
The Trap Behind the Advice: Where Does That Flag Live?
Here is where most write-ups stop, and where the second bug begins. A developer reads “check once, cache the verdict” and reaches for the obvious implementation:
// The same trap in three costumes — each "looks reasonable, isn't":
std::atomic<bool> g_licensed { false }; // 1. a global
class LicenseCache
{
static LicenseChecker checker; // 2. a class-static member
};
bool isLicensed()
{
static LicenseChecker checker; // 3. a function-local static
return checker.verdict();
}
The g_ prefix in the first line is a courtesy to the reader; the compiler doesn’t need it, and the second and third forms don’t advertise themselves at all. What the three share is what matters: static storage duration. One copy per loaded binary. A lifetime that begins when the binary loads (or at first call) and ends only when the binary unloads — tied to the module, not to any plugin instance. And no owner: no destructor you invoke, no defined moment when tearing it down is safe. The atomic is fine. The storage duration is the problem — because of a fact that’s easy to state and easy to forget: a DAW is one process, and your plugin does not have it to itself.
When a producer loads twelve instances of your compressor across a session, the host loads your binary once and creates twelve instances inside the same process. Every static and global in your code is shared by all twelve. And if you ship a product line — a compressor, an EQ, a reverb — linked against a shared framework of yours, instances of different products can share those statics too. Your licensing state is suddenly a tiny piece of shared infrastructure, and it inherits every classic shared-state failure:
- Initialization races. Two instances constructed concurrently — hosts do instantiate plugins from multiple threads — both find the checker uninitialized and both initialize it. Function-local statics have been thread-safe since C++11, but the work they trigger (spawning threads, reading files, network calls) still interleaves in ways nobody designed.
- Cross-instance surprises. The user deactivates the license from one instance’s UI, and eleven other instances flip to unlicensed mid-render — from a code path that was written and tested as if it owned the world. Sharing a verdict across instances may even be what you want; the point is that with a static, it happens whether you designed it or not.
- Teardown under a moving train. This is the one that crashes DAWs. The user removes the last instance; the host unloads your binary. If a static-owned background thread — a periodic re-checker, a retry loop, that license thread from the sketch above — is still running when the code it’s executing is unmapped from memory, the process doesn’t get an exception. It gets undefined behavior inside the host, and the crash report lands on the DAW vendor’s desk with your name buried in the stack trace.
Note that the trap is about lifetime, not spelling. Hide the flag as an ordinary member of a long-lived “manager” object — one that is itself kept alive by something static — and you have changed nothing except how many hops it takes to see the problem. The question to ask of any piece of license state is not “is it marked static?” but “whose lifetime is it tied to, and who ends it?” If the honest answer is “the binary’s, and nobody,” you are in the trap regardless of syntax.
C++ veterans will recognize the deeper disease here: this is the static initialization order fiasco wearing headphones. Objects the compiler places — globals, class-statics — are constructed as the binary loads, before any code of yours runs, across translation units in an order the standard leaves undefined; they are destroyed in reverse at a moment you don’t choose, and on Windows all of it happens while the loader holds its lock, where spawning or joining a thread is a deadlock waiting to happen. The classic desktop-era cure was Scott Meyers’ construct on first use idiom: don’t let the compiler place the object at all — create it on the heap, at a moment you choose, and (in the traditional version) deliberately never delete it, so destruction order can’t hurt you. Half of that advice survives inside a plugin. Heap placement at a chosen moment is exactly right — it is the seed of the per-instance pattern below. But the leak-it-and-forget half dies at unload: a DAW really does unmap your binary, and a leaked object whose background thread is still running crashes the host just as surely as a static would. In a plugin there is no forgetting; someone must own the teardown.
Plugin validators and DAW plugin scanners make this concrete: they load, instantiate, destroy and unload plugins rapidly, in tight loops, exactly the sequence that flushes out static-lifetime bugs. If your plugin passes a scan flakily — works nine times, crashes the tenth — static state with a background thread is one of the first places to look.
Per-Instance by Default, Shared by Design
The resolution is a rule of thumb with one deliberate exception.
Default: license state is a member of the instance. The atomic flag, the checker, the background thread — all members of your processor, as in the sketch above. Twelve instances means twelve flags and twelve short-lived validation calls; in exchange, there is no shared state, no init race, no cross-talk, and teardown is exactly the destructor of each instance joining its own thread. Twelve validations at session load is not a real cost — each is one small request at construction time, off the audio thread, and any licensing server worth using absorbs that without blinking.
Exception: share deliberately, never accidentally. If you have a real reason to validate once per process — a heavyweight check, a strict rate limit, a shared hardware probe — then build the sharing as an explicit, reference-counted object: created by the first instance that needs it (thread-safely), handed to the others, torn down — background thread joined and gone — when the last owner releases it. Its lifetime is tied to the instances that use it, never to the lifetime of the loaded binary. JUCE users will recognize this pattern as exactly what juce::SharedResourcePointer exists for; outside JUCE, a std::shared_ptr handed out by a small thread-safe factory does the same job.
The distinction is not per-instance versus shared. It’s owned versus ambient. State with an owner has a defined start, a defined end, and a defined answer to “who joins the thread.” A bare static has none of those — it has whatever the dynamic loader and the C++ runtime’s destruction order give it that day.
What to Ask of a Licensing SDK
Everything above applies whether the licensing code is yours or a vendor’s. If you build your own license system, you own these problems directly. If you integrate an SDK — ours or anyone’s — you’re linking someone else’s code into a process the DAW owns, and you’re entitled to blunt answers to four questions:
- Which calls block? Every call that can touch network or disk should be documented as blocking or non-blocking. “It’s usually fast” is not an answer; the audio thread doesn’t care about usually.
- Is there hidden global state? Can two instances — or two different products — use the SDK in one process without stepping on each other?
- Does any call leave a thread running after it returns? If yes, who stops it, and what happens at plugin unload?
- Are process-global dependencies handled? Common C libraries (libcurl is the classic) require one-time process-wide initialization that is not thread-safe to repeat. Inside a plugin, the SDK must guard that itself — you have no
main()to do it in.
Since this guide doubles as documentation of our own SDK, here is how the Keyzy C++ client answers, plainly: every Keyzy call blocks, by design. validateOnline() performs a network round trip; validateOffline() reads and decrypts a file from disk. None of them are audio-thread-safe, none pretend to be, and the threading model is deliberately yours — the SDK does not spawn anything that outlives a call, so no Keyzy thread can exist at unload time. There is no global mutable license state: each validator instance you create owns its own state, and any number of them coexist in one process. The single process-global dependency (libcurl initialization) is wrapped in a std::call_once inside the SDK, so concurrent first use from multiple instances is safe. The integration pattern in this guide — validate in the background, cache the verdict, read an atomic in processBlock — is not just compatible with the SDK; it is the intended way to use it.
Bottom Line
Four rules carry the whole guide:
- No licensing code on the audio thread — not online, not offline, not “just one file read.”
- Validate in the background, cache the verdict, read an atomic — and degrade gracefully, never abruptly.
- License state belongs to an instance, not to a static — a DAW is one process, and your plugin shares it.
- If you share state, share it deliberately — explicit ownership, thread-safe creation, threads joined by the last owner.
None of this is exotic. It’s the same real-time and shared-state discipline audio developers already apply to DSP and GUI code — extended to the one subsystem that’s easiest to treat as an afterthought, because it isn’t the product. But it runs inside the same deadline as the product, in the same process as the product, and when it misbehaves, the user hears it.
If you’d rather start from a working example than a blank file, the C++ Quick Start shows a complete integration, and the features page covers what the platform handles beyond validation.