Last year a fintech client asked us to run real-time fraud scoring on transaction streams without ever seeing the plaintext. Their compliance team wouldn't budge on plaintext exposure at third-party PoPs, and our edge nodes weren't in scope for PCI-DSS level 1. We've all hit that wall before — you either terminate TLS and decrypt at the edge and absorb the liability, or you backhaul everything to a centralized HSM and blow your 40ms latency budget. Homomorphic encryption finally gave us a third path, and it's reshaping how we think about trust at the edge.
The Decrypt-and-Compute Trap at Untrusted Edges
For a decade our edge security model was straightforward: TLS terminates at the PoP, we decrypt, we run business logic, we re-encrypt. It works fine for caching and header manipulation, but it collapses the moment you process sensitive payloads. I remember auditing an edge worker that was doing personalized pricing for a healthcare provider — the worker had full access to age, diagnosis codes, and zip code in memory. The code was clean, the node was hardened, but from a cryptographic standpoint we were just promising not to look. That's not a security guarantee, that's a policy.
The alternative hasn't been much better. We've tried confidential computing enclaves and tokenization gateways, and while they help, they don't solve the fundamental problem: data has to be in the clear to be computed on. Even with HTTP Working Group Specifications pushing for encrypted ClientHello and Oblivious HTTP, the moment your edge function calls JSON.parse() you've exposed the payload. If you're handling PII, financial scoring, or private telemetry, the edge becomes a compliance minefield you can't easily staff or certify.
Homomorphic encryption flips that assumption. It lets an edge worker evaluate a function f on ciphertext Enc(m) and produce Enc(f(m)) without ever possessing the secret key. The client encrypts locally, the CDN computes blind, and only the client can decrypt the result. You're not trusting the PoP to behave — you don't need to. This isn't theory anymore; with lattice-based schemes running in WebAssembly, we can execute additive and multiplicative operations directly inside a Cloudflare Worker or Fastly Compute isolate without a sidecar enclave.
// Edge worker: additive aggregation on Paillier/BFV ciphertexts
// Client sends Enc(a) and Enc(b), edge returns Enc(a + b) without decryption
import { Evaluator, CipherText } from 'node-seal-wasm';
export default {
async fetch(request, env) {
const { encA, encB, publicContext } = await request.json();
const seal = await env.SEAL_CONTEXT(publicContext);
const evaluator = seal.Evaluator();
// Deserialize ciphertexts sent by client (already encrypted under client key)
const cipherA = CipherText.fromBase64(encA, seal);
const cipherB = CipherText.fromBase64(encB, seal);
const cipherResult = seal.CipherText();
// Homomorphic addition: Enc(a) + Enc(b) = Enc(a + b)
evaluator.add(cipherA, cipherB, cipherResult);
// We can't decrypt - no secret key on the edge. Just forward.
return new Response(JSON.stringify({
encResult: cipherResult.toBase64(),
// Noise budget remaining - client will need this
noiseBudget: seal.invariantNoiseBudget(cipherResult)
}), { headers: { 'content-type': 'application/json' } });
}
}
Picking a Scheme When Your Compute Is a 128MB Isolate
Not all homomorphic schemes belong at the edge. I've seen teams try to shoehorn full bootstrapping TFHE into a 50ms worker and wonder why their p99 spiked to four seconds. You've got to match the homomorphism to the operation and the constraint. Partially homomorphic schemes like Paillier (addition only) or ElGamal (multiplication only) are brutally efficient — single-digit milliseconds and tiny ciphertexts — but they can't handle anything beyond one operation type. If you're just doing private counters, vote tallying, or encrypted CDN log aggregation where you sum blinded metrics, they're perfect.
For most real CDN use cases you need somewhat or leveled FHE. BFV and BGV give you exact integer arithmetic, which is ideal for billing, auction logic, or private set intersection at the edge. CKKS is the one I've used most for ML inference — it does approximate arithmetic on real numbers, so you can run a linear model or anomaly detector on encrypted feature vectors and accept a tiny precision loss. The catch is noise growth. Every multiplication consumes noise budget, and once it's gone you either need to bootstrap (expensive) or you've pre-parameterized your depth correctly. On edge isolates we don't bootstrap; we design leveled circuits with a fixed multiplicative depth of 3-5 and fail fast if the client sends parameters that would exceed it. It's similar to how we handle resource limits for WebGPU Edge Rendering: Offloading 3D Computation to CDN Infrastructure — you can't treat the edge like an unbounded origin VM.
TFHE and FHEW give you fast bootstrapped boolean gates, which sounds attractive for decision trees, but their ciphertext expansion and key sizes are punishing for last-mile delivery. I've benchmarked TFHE bootstrapping on a 2 vCPU edge instance at ~35ms per gate — fine for a single comparison, catastrophic for a 100-node tree. My rule of thumb: if your edge logic can be expressed as low-degree polynomials (dot products, weighted sums, simple scoring), use BFV/CKKS with precomputed parameters. If you need comparisons and branching, consider hybrid approaches where the edge does the homomorphic linear part and the client does a single garbled-circuit step for the threshold. Guidance on isolating crypto-heavy work from main thread rendering, as described in web.dev Articles on WebAssembly performance, applies directly here.
// CKKS: private fraud score = w1*x1 + w2*x2 + w3*x3 on encrypted inputs
// Edge has plaintext weights, client provides encrypted features
import { CKKSEncoder, Encryptor, Evaluator } from 'node-seal-wasm';
async function scoreEncryptedTransaction(seal, encFeatures, plainWeights) {
const encoder = seal.CKKSEncoder();
const evaluator = seal.Evaluator();
const scale = Math.pow(2, 40);
// Encode weights as plaintexts at same scale
const ptW1 = encoder.encode(plainWeights.w1, scale);
const ptW2 = encoder.encode(plainWeights.w2, scale);
const ptW3 = encoder.encode(plainWeights.w3, scale);
// encFeatures = [Enc(x1), Enc(x2), Enc(x3)] from client
const tmp1 = seal.CipherText(); evaluator.multiplyPlain(encFeatures[0], ptW1, tmp1);
const tmp2 = seal.CipherText(); evaluator.multiplyPlain(encFeatures[1], ptW2, tmp2);
const tmp3 = seal.CipherText(); evaluator.multiplyPlain(encFeatures[2], ptW3, tmp3);
evaluator.rescaleToNextInplace(tmp1); evaluator.rescaleToNextInplace(tmp2);
evaluator.rescaleToNextInplace(tmp3);
// Homomorphic sum -> Enc(score)
const sum12 = seal.CipherText(); evaluator.add(tmp1, tmp2, sum12);
const encScore = seal.CipherText(); evaluator.add(sum12, tmp3, encScore);
// No decryption capability here - return to client for decryption
return encScore;
}
What It Actually Costs at the Edge: Latency, Size, and Noise
Let's be blunt about overhead. I deployed a leveled BFV counter aggregation to 12 PoPs last quarter and watched our median worker CPU time jump from 4ms to 38ms. Ciphertexts aren't JSON — a single BFV ciphertext with n=4096 and 109-bit coefficients is ~64KB. Send 100 encrypted feature vectors and you've added 6.4MB to a request that used to be 2KB. You'll hit CDN for Spatial Computing: Delivering AR/VR Assets with Sub-Frame Latency style payload constraints faster than you expect, and HTTP/3 helps with transport but doesn't fix the math.
Parameter selection is where most teams get burned. Choose a polynomial degree that's too small and you run out of noise budget after two multiplications; too large and your ciphertexts balloon and your NTT transforms crawl. We've standardized on n=8192 for CKKS scoring workloads — it gives us ~5 multiplicative levels with 40-bit scale and keeps ciphertexts under 256KB, which still fits inside our edge request limits without chunking. For pure additive telemetry like private analytics rollups, we drop to n=4096 Paillier-style BFV and stay under 15ms per aggregation. And don't forget key context distribution: the evaluation keys (relinearization and Galois keys) can be 10-20MB. We cache them at the PoP on first use and pin them in KV, otherwise you're paying that download on every cold start.
The table below is from our staging benchmarks on a single vCPU isolate (128MB, WASM runtime) — not a lab server. These aren't cherry-picked; they reflect what you'll see if you deploy this week. Unlike WebXR Content Delivery Through CDN: Streaming Immersive Experiences at Low Latency where we can trade quality for bitrate, here you trade circuit depth for performance, and there's no adaptive bitrate to save you.
| Scheme & Params | Operation Type | p95 Worker CPU | Ciphertext Size | Noise Budget After Op |
|---|---|---|---|---|
| Paillier 2048-bit | Enc(Add) - aggregation | 8 ms | 0.5 KB | N/A (additive only) |
| BFV n=4096, 109-bit | 1x Add + 1x Multiply | 34 ms | 64 KB | ~68 bits remaining |
| CKKS n=8192, scale 2^40 | Dot product (3 features) | 112 ms | 210 KB | Putting It Together: A Working BFV Aggregation Worker at the Edge