← Blog

Shadow AI and local AI: the data class decides

· 22 min read

AI engineeringLocal LLMsSecurityArchitecture

The conversation about where AI should run is usually framed as local versus cloud, and the conversation about employees using unapproved AI is usually framed as a discipline problem. Both framings are wrong in the same way: they skip the question that actually decides things – what kind of data is involved, and how much control that data needs.

The spine of this post. It isn't local versus cloud: the data class picks the level of control. There are four levels, not two. For local, the burden of proof is on local. And shadow AI is a product problem, not a disciplinary one.

Four levels, not two

1 · Local / on-premOllama, vLLMyou controlhardwarenetworklogs + auditmodel versionair-gap · offline · contracts2 · Your cloud tenantAzure OpenAI, Bedrockyou controlnetwork (VNet)identity + RBAClogs + auditthe right default3 · Frontier API, directa contract with the vendoryou controlcontractretention termsnewest models, connectors4 · Consumer appa personal accountyou controlnothing= Shadow AIcontrol decreases →
Fig. 1 · Level 2 is the one people talk about least. The request doesn't go “to OpenAI”, it goes into your own cloud: your network, a private endpoint, a managed identity, your logs. Level 4 is what happens when none of the first three is offered.

Once there are four levels, the useful question is no longer "which one is best" but "which data may go where". That needs a data classification, and it needs to be short enough to remember.

1 · local2 · your tenant3 · direct API4 · consumer appPublicalready published✓fine✓fine✓fine~toleratedInternalown code, docs, tickets✓fine✓fine✓with a contract✗policy violationRestrictedcustomer, health, legal data✓fine✓if contract allows✗usually not✗an incidentThe data class picks the level — not the other way round. Three classes you can recite beat seven nobody remembers.
Fig. 2 · An example policy, not the policy. The bottom-right corner matters most: pasting your own code into a personal chatbot and pasting customer data there are not the same offence. The first is a policy violation; the second is an incident. A rulebook that forbids both identically will see neither rule kept.

Local AI: the arithmetic

Most disappointment with local models comes from three numbers nobody checked before buying hardware. All three follow from how a language model produces text: one token at a time, each after the other. (A token is roughly a syllable-sized piece, not a word – and languages like Hungarian need noticeably more of them per sentence than English.)

Two speeds, and you always feel the worse one

Chat turnprefilldecode — the answer types itself outAgent turnprefill — re-reads half the codebase, every turndecodetime →prefill is compute-bound: the GPU mattersdecode is memory-bound: GB/s matters
Fig. 3 · Prefill is the “thinking…” pause before the first character; decode is the classic tokens-per-second number. For chat, decode dominates. For a coding agent, prefill does – the answer is written quickly and the whole thing still feels slow.
reading speedenough in theory, painful in practicecomfortable chatreasoning models · coding agentsagents also need fast prefill58152050100tokens / sReasoning: 500 thinking tokens at 15 tok/s = 33 seconds of staring at nothing.Batch, classification, embeddings: 5 tok/s is plenty — nobody is waiting.
Fig. 4 · What is liveable. Thinking tokens aren't read by anyone – they are pure waiting – which is why reasoning models raise the floor so sharply. It is also why the sweet spot of local AI is not chat but batch: work nobody is waiting for runs perfectly well at 5 tok/s.

Memory bandwidth is the key, not GPU muscle

To produce each token, the model reads its active weights from memory. So decode speed is, to a first approximation, memory bandwidth divided by the bytes read per token. That one formula explains nearly everything you will measure.

256 GB/s unified memory — the Strix Halo / DGX Spark class020406080tok/sDense 70Breads ~40 GB for every token6.4 · ceiling = 256 ÷ 40~5 measuredMoE 120B~5B active → reads ~3 GB per token~85 · ceiling = 256 ÷ 334–53 measuredtok/s ≈ memory bandwidth ÷ bytes read per token. The ceiling ignores KV-cache reads and compute; reality sits below it.
Fig. 5 · A dense 70B model at 4-bit is about 40 GB, and all of it is read for every token: 256 ÷ 40 ≈ 6 tok/s. Everyone measures about that – it isn't a driver problem, it is physics. A mixture-of-experts model activates only a fraction of itself per token, which is why a 120B MoE runs at 34–53 tok/s on the same hardware. Without MoE, the 128 GB desktop-box category would not exist.

So the number to look for on a spec sheet is GB/s, not TOPS or core counts. The unified-memory boxes sit around 256–273 GB/s; an M3 Ultra is about 819; a discrete GPU is around 1,000 and up – with far less memory attached. Prefill, on the other hand, is compute, and there the GPU matters: on the same 120B model an early llama.cpp benchmark measured 1,723 tok/s of prompt processing on a DGX Spark against 340 on a Strix Halo, while decode differed by only about 13% (38.6 against 34.1 tok/s). Later builds narrowed the prefill gap to roughly 2×. For chat you would never notice. For an agent it is the difference between usable and not.

What fits – and what that leaves room for

Rule of thumb: at 4-bit quantisation, the usual default, size in GB ≈ half the parameter count in billions. 70B is 35–40 GB, 120B is 60–70 GB, 235B is 115–120 GB. But the weights are not the only tenant of that memory. Every open conversation keeps a KV cache that grows with context length – and every concurrent user has their own.

128 GB box · dense 70B at Q4 · every user holds a 100k-token context1 userweights 40 GBKV 32 GB72 GB2 usersweights 40 GBKV 32 GBKV 32 GB104 GB3 usersweights 40 GBKV 32 GBKV 32 GBKV 32 GB136 GB ✗128 GBKV cache, Llama-3-70B class: 80 layers × 8 KV heads × 128 dims × 2 (K+V) × 2 bytes ≈ 0.32 MB per token.Big model, long context, many users: pick two.
Fig. 6 · Large model, long context, many users: pick two. And “it fits” is not “it is usable” – a 235B model does squeeze into 128 GB, at single-digit tok/s and with no room left for context. That is a demo, not a tool.

Concurrency: the number missing from every ROI slide

One of these boxes serves one user comfortably, two with goodwill – not ten. Memory is one reason (Fig. 6). Prefill is another: it is compute-bound, so one person's 60,000-token prompt stalls everyone else's first token. And batching, which rescues throughput for dense models, helps less with MoE, because different users wake different experts and the bytes read per step go back up. Five developers sharing one box means queueing plus single-digit tok/s each.

Total cost

A capable box costs in the region of €4,000–5,000, plus electricity, plus someone to operate it. One person's cloud token spend is on the order of €25–100 a month. At the top of that range the hardware pays for itself in about 50 months – for one person, before power and operations. And the honest amortisation period is not five years but about 24 months: model architectures shift, and inference software keeps getting substantially faster on the hardware you didn't buy. The contrarian advice that follows: if you don't have a real air gap, rent a dedicated GPU for a year instead of buying metal. Buying makes sense for stable, high-volume batch work without bursts.

Why local carries the burden of proof

Local is the right answer for: a genuine air gap; a contract that forbids sub-processors; raw health or legal data; high-volume, low-sensitivity batch; offline and edge deployments, where the missing network is the deciding factor rather than secrecy; and fine-tuning. It is the wrong answer for "it feels safer". With local AI the attack surface doesn't disappear – it moves in with you. An unpatched internal inference server without authentication is a larger risk than Azure OpenAI behind a private endpoint.

Local needs the stronger justification not because it is technically worse, but because reversibility is asymmetric.

Wrong clouddecisiona monthly invoiceopexswitch or cancelabout two weeksundonecost: one bad invoiceWrong localdecisiona signed POcapex · asset tagstill in the server room 24 months laterdepreciating, with your signature on it
Fig. 7 · A bad cloud decision is a monthly invoice. A bad local decision stands in the server room, signed – internally a matter of prestige, externally a reference risk.

Before the purchase order, have answers to these:

  1. The concrete use case, and the data class it involves.
  2. How many concurrent users – measured, not guessed.
  3. The typical prompt size, because that decides whether prefill or decode is your bottleneck.
  4. Whether there is an eval. If there is none, the project has already failed: you will have nothing to defend it with, and nothing to refute it with.
  5. Who operates it – a name, not a team.
  6. The exit plan.

The model is a file; the rest is harness

A model really is a file. But what a user experiences as "the model" is almost entirely what sits around the weights – and that is where local set-ups succeed or fail.

your appthe harness — what users experience as “the model”chat templateformats the promptinference enginebatching · KV / prompt cachemodel weightsone file · quantisedstructured outputconstrained decodingtool-call parserbreaks first, locallyQuantisation barely moves benchmark scores. It degrades tool calling, JSON output and long-context recall first.
Fig. 8 · Same weights, different harness, different product. Locally it is rarely the model's quality that lets you down. It is the tool-call parser, or a chat template that doesn't match the one the model was trained with.
  • Engines. Ollama and LM Studio are developer convenience for one user. vLLM and TensorRT-LLM are production serving, with continuous batching. Whoever plans to serve twenty people from Ollama started in the wrong place. (Licensing, since it comes up: LM Studio has been free for work use since July 2025; you just can't redistribute or embed it.)
  • Quantisation is not "a slightly dumber model". Benchmark scores barely move at 4-bit – but tool calling, JSON output and long-context instruction-following degrade first. Precisely what agentic use needs, and precisely what nobody measures while quoting MMLU.

Chinese open-weight models: split the question in three

QuestionAnswer
Does data go to China?Run locally: no. Weights plus arithmetic – a GGUF file does not phone home.
Is the licence a problem?The reverse of what people assume: Qwen and DeepSeek are largely Apache 2.0 / MIT, more permissive than the Llama community licence.
What remains?Bias on certain topics, and supply chain: where you downloaded it, checksums, who produced the quantised build and the chat template.

The risk isn't in the model; it is wherever you call an API. The same DeepSeek on its hosted chat and on your own hardware are two entirely separate risk profiles.

The LLM provider is an adapter

For a boxed, on-premises product there is no "we decide". The customer decides, and every customer decides differently. So the LLM provider is an adapter – exactly like the database, the login and the file store in the micromonolith post.

The wire protocol is already solved: the OpenAI-compatible /v1/chat/completions is a de facto standard across Azure, vLLM, Ollama, LM Studio and OpenRouter. The hard part is what is not uniform behind it: how reliable tool calling is; structured output (real constrained decoding versus "please return JSON"); usable versus nominal context; content filtering (present on Azure, absent locally); rate-limit semantics and retry hints.

applicationILlmProviderone port + capabilitiesAzure OpenAIvLLM · Ollamafrontier APIall speak /v1/chat/completionseach adapter reportstoolCallingconstrainedJsonusableContextcontentFilterneed JSONconstrainedJson?yesschema-enforced decodingnoprompt → validate → retrysame result,different cost
Fig. 9 · One adapter per level a customer might choose – their tenant (level 2), a local engine (level 1), a direct API (level 3). Design by capability discovery, not by if-statements on provider names. Degrade gracefully, and keep a zero-config default so the product starts before anyone has chosen a provider.
public enum ToolCalling { None, BestEffort, Reliable }

public sealed record LlmCapabilities(
    ToolCalling ToolCalling,
    bool ConstrainedJson,        // can the engine enforce a JSON schema while decoding?
    int UsableContextTokens,     // what actually works, not what the model card says
    bool ContentFilter);         // present on Azure, absent on a local engine

public interface ILlmProvider
{
    LlmCapabilities Capabilities { get; }
    Task<ChatResult> ChatAsync(ChatRequest request, CancellationToken ct);
    Task<T> ChatJsonAsync<T>(ChatRequest request, JsonSchema schema, CancellationToken ct);
}
// branch on what the provider can do, never on what it is called
public async Task<T> GetJsonAsync<T>(ChatRequest request, CancellationToken ct)
{
    if (provider.Capabilities.ConstrainedJson)
        return await provider.ChatJsonAsync<T>(request, JsonSchema.For<T>(), ct);

    for (var attempt = 1; attempt <= 3; attempt++)
    {
        var reply = await provider.ChatAsync(request.WithJsonInstructions<T>(), ct);
        if (JsonValidator.TryParse<T>(reply.Text, out var value))
            return value;

        request = request.WithFeedback("The reply was not valid JSON for the schema. Try again.");
    }

    throw new LlmOutputException("No valid JSON after 3 attempts.");
}

The lock-in is the embedding model, not the chat model

Swap the chat model: configurationappchat model Achat model Bone line of config · restart · doneSwap the embedding model: migrationvector index3072-dim · text-embedding-3-large✗bge-m31024-dim · other spacere-embed every document · rebuild the indexa schema migration, not a settingso the index records which model and dimension built it
Fig. 10 · The chat model is a configuration decision. The embedding model is a schema decision. You can replace the chat provider tomorrow; replacing the embedding model means re-indexing everything.

Therefore the embedding model's name and dimension are part of the schema, and the index records what built it:

{
  "index": "documents-v3",
  "embeddingModel": "text-embedding-3-large",
  "dimensions": 3072,
  "chunking": "v2"
}

The hidden cost of all this flexibility: every supported provider needs its own regression eval. Three providers is not three times the code – it is three times the evals and three times the support.

Your own cloud tenant

My own default, for what it's worth: internal tools run on Azure OpenAI in our own tenant – a GPT model plus text-embedding-3-large for vectorisation. Even in a strongly technical team, "bring it on-prem" is not the default. And this is exactly where the embedding lock-in bites: if we moved local tomorrow, we wouldn't be swapping a chat model, we would be rebuilding the whole index.

Level 1local / on-premappinference servervLLM · Ollama · your GPUyour logsand your patchingLevel 2your cloud tenantappmanaged identityprivate endpointinside your VNetmodel deploymentAzure OpenAI · Bedrockyour logsRBAC · quotasLevel 3direct APIappAPI key in configpublic internetprovider APItheir logs · your contractLevel 4consumer appa browser tabpersonal accountpublic internetconsumer apptheir logs · no contractDashed outline = a boundary you own. Going down, the prompt travels further from you, and you can prove less about it.
Fig. 11 · The four levels from Fig. 1 again, this time as the path one prompt takes. Level 2 is the only one that gives you cloud models and a boundary you own: private networking, no keys in code, role-based access, a content-filter level you choose, token quotas per team as cost control – and every prompt loggable, which is both your audit trail and your best weapon against shadow AI.

The downsides to accept knowingly: new model versions arrive later; model availability per region is a lottery; quota administration is real work; and your SLA hangs on their capacity.

The region argument – say it the right way

The wrong way: "It's in West Europe, so the data never leaves Europe, so GDPR is covered." A data-protection officer or a lawyer shoots that down in one sentence, and everything you say afterwards is suspect.

EU region — for example West Europedata centrestored + processed hereyour tenantDPA · retention termsdata residency ✓documented, auditable, signedCLOUD Actfollows the companyUS parent companysubject to US lawdata sovereignty ✗wherever the hardware standsNot “the data never leaves Europe” — but “the risk is known, documented, accepted, and somebody signed it”.
Fig. 12 · Data residency is not data sovereignty. The CLOUD Act follows the company, not the data centre.
An EU region determines where the data is physically stored and processed – data residency. That has real value under GDPR: processing stays in the EU, the provider's role as processor is fixed in the DPA, retention is governed by terms you can see, and the whole arrangement is documentable and auditable. What an EU region does not give you is data sovereignty: the CLOUD Act binds the US parent company regardless of where the hardware stands. So the right sentence is not "the data never leaves Europe" but "the risk is known, documented and acceptable – and somebody signed it".
  • A defensible baseline. DPA, EU region, controlled retention and a documented data flow are enough for the overwhelming majority of use cases. Supervisors don't expect zero risk; they expect a reasoned, written decision.
  • Latency and cost. Not a compliance argument at all – an engineering one, and sufficient on its own to justify the region.
  • The asymmetry, reversed. If everyone flees into an air gap because of the CLOUD Act, the data ends up on a badly run server of your own. The CLOUD Act is a theoretical risk. The unpatched internal server is a practical one.

What "region" does not cover, and you must check separately: storage location and processing location are separate fields; retention (on Azure, abuse-monitoring retention can be switched off for approved use cases); sub-processors; support staff access; governing law. That is why it can't be settled in one sentence – and also exactly what makes it documentable. If someone pushes further, the next steps are a sovereign cloud (Bleu, Delos, the AWS European Sovereign Cloud) or genuine on-prem; say out loud that this is a narrow, regulated group, not the base case. None of this is legal advice – it is the wording a DPO doesn't reject after the first sentence.

Two ways a RAG system leaks

Leak 1 · no security trimmingan internRAG botindexboard-salaries.pdfthe index ignores the ACL the file share enforcedfix: filter by the asker’s permissions before retrievalthe index is a data class of its ownLeak 2 · local model, cloud embeddingssensitive docsembedding API · cloudvector storelocal LLM ✓the text you kept local just left — to be embeddedfix: model local ⇒ embeddings and vector store local
Fig. 13 · The most common enterprise RAG failure is a bot that tells people things they have no right to see. Security trimming is the first feature, not an option. And if the model goes local, the embedding model and the vector store go with it.

The underlying mistake in both panels is the same: forgetting that the index is itself a data class. It is exactly as sensitive as its sources, and it rarely carries the same permissions.

Shadow AI

FindingSource
Employees regularly using AI on corporate devices: 15% → 45% in one year. Shadow AI is now the third most common non-malicious insider action; source code is the data most often uploaded.Verizon DBIR 2026
66% of office professionals used AI at work while believing it was not permitted. 88% shared work information with public AI tools; 34% shared customer data.PagerDuty 2026
Shadow AI was a factor in 20% of breaches and added about $670,000 to the average cost. 97% of organisations with an AI-related incident lacked proper AI access controls.IBM, 2025
47% of generative-AI users in enterprises reach the tools through personal accounts.Netskope, 2026
About 11% of what employees paste into chatbots is confidential.Cyberhaven, 2023

Why it happens is no mystery – it is Shadow IT again. Nobody spun up a VM behind the device-management tooling out of malice; they wanted to get work done. The difference is that the gap between the good tool and the bad tool used to be 10%, and now it is a multiple. Under that much pressure, policy does not hold the wall. What is missing is not prohibition but an alternative.

A ban is not control. All a ban achieves is that you can no longer see what is happening.
none of these is a ChatGPT taba developer’s laptopand everything it can reachpersonal API keyin the IDE or CLI — no licence neededunaudited MCP serversthe agent acts: DB, mail, filesbrowser extensionsfull page access, zero visibility“sanctioned” shadow AISaaS AI features nobody reviewedAn MCP server started with one npx command can do whatever the developer’s machine can do.
Fig. 14 · The forms that matter now. MCP servers are the most serious because an agent doesn't just read – it acts. “Sanctioned” shadow AI is the most embarrassing: Notion AI, Slack AI, Zoom AI – procurement signed it, security doesn't know, nobody turned it off.

Detection: don't start with DLP

Start with what you already have: DNS and proxy logs; a review of the OAuth grants in your SSO (ten minutes, startling results); personal subscriptions on company cards. Only then CASB and DLP.

What works

“I need AI for this”official requestvendor reviewsix weeksapprovedpersonal accounttwo minuteswork gets done — invisiblyno log, no contract, no controlpaved road: approved in one day →the detour stops being worth it
Fig. 15 · People take the faster road. You cannot out-policy that; you can only make the official road faster.
  1. A paved road that is genuinely fast – one day, not a six-week vendor review.
  2. Three data classes, not seven. If you can't recite it, it is too complicated (Fig. 2).
  3. An amnesty with a time window – "tell us now; in two weeks it becomes a problem".
  4. Measure usage, not compliance. If training-completion rates go up, you have won nothing.

Who is to blame

Everyone, asymmetrically. The company more: it holds the decision, the money and the procurement process, and if it provides no tool for six months, that was its choice. But the developer carries professional responsibility – "the process was inconvenient" is not a defence in a GDPR incident. And the distinction from Fig. 2 stands: pasting your own code is a policy violation; pasting customer data is an incident.

The AI Act, in four lines

2 Aug 2026Article 50 transparency appliesnot postponed by the Omnibus2 Dec 2026marking of AI-generated contentgrace period ends for existing systems2 Dec 2027high-risk (Annex III) obligationspostponed by the OmnibusTransparency duties bind deployers too, not only model providers. The deadline moved; the architecture didn’t.
Fig. 16 · The Digital Omnibus was agreed in May 2026 and formally adopted in June. It moved the high-risk dates. It did not move Article 50.

The practical consequence for anyone shipping AI features: people must be able to tell when they are talking to an AI, and AI-generated content must be identifiable as such – and that duty sits with the deployer as well as with the model vendor.

Objections, and the answers

They sayYou say
"Local AI is the privacy solution."It is the control solution. Privacy is a process, not a box.
"One strong box is enough for the team."One box is one or two concurrent users. Let’s look at the arithmetic.
"Cloud is more expensive."Only if operating hours cost nothing and you amortise over five years.
"Chinese model = data leak."Not when it runs locally. The risks are bias and supply chain.
"EU region = GDPR ticked."Residency is not sovereignty. But DPA + EU region + controlled retention is defensible.
"Ban it until there is a policy."A ban removes visibility, not usage.
"Let’s buy the hardware and figure out what for."The burden of proof runs the other way: use case, eval, concurrency – then the PO.
"We’ll just swap the model later."The chat model, yes. The embedding model means a full re-index.

Takeaways

  1. Data class → control level. Four levels; your own cloud tenant is the right default.
  2. tok/s ≈ bandwidth ÷ bytes read per token. Read GB/s on the spec sheet; prefill is the exception that needs compute.
  3. Large model, long context, many users: pick two. One box is one or two concurrent users.
  4. Local must justify itself – use case, eval, measured concurrency, named operator, exit plan – because capex is hard to undo.
  5. What you call "the model" is mostly the harness. Quantisation hurts structured output first.
  6. Make the provider an adapter, branch on capabilities, and treat the embedding model as schema.
  7. Residency is not sovereignty – say the defensible sentence, not the comfortable one.
  8. Shadow AI is a product problem. Build a road faster than the detour, and keep the logs.

Sources