Skip to content
NLEN
Illustration: PII anonymization in payloads before external LLM transmission

PII anonymization in payloads before sending to an external LLM

By Ivo Donker — compiled with AI assistance (Claude & Gemini)

When an application interacts with external AI providers, sending raw user input poses a significant privacy and compliance risk. Directly identifiable personal data (Personally Identifiable Information, or PII) such as national ID numbers, credit card numbers, email addresses, IBAN account numbers, and personal names must not be forwarded uncontrolled to external processors. Even when contractual data processing agreements are in place, a defensive software architecture requires that sensitive data never leave your own infrastructure boundary unencoded.

Within a secure API architecture, protecting network payloads forms the logical complement to key management; see the guide on managing LLM API keys securely to see how you prevent unauthorized access to endpoints on the provider side. PII removal and replacement, however, requires a specific operational transformation pattern. In this article we cover the failure modes, detection techniques, sanitization strategies, and reversible pseudonymization tunnels needed to deterministically strip payloads of personal data before transmission over the public network.

The failure modes of unfiltered payload transmission

Directly forwarding raw prompts to external endpoints leads to four specific failure modes in production environments:

Detecting these risks primarily happens via automated payload auditing, in which outgoing HTTP requests are sampled and scanned for known PII patterns. The structural mitigation consists of placing a middleware component or gateway layer that parses incoming payloads, recognizes entities, replaces them with abstract tokens, and optionally decodes the resulting answer again (de-pseudonymization).

Detection mechanisms: regex, NLP, and hybrid pipelines

Identifying personal data in unstructured prompt text requires several analysis layers. No single detection mechanism offers full coverage on its own.

Detection method Target entities Advantages Failure modes and limitations
Regular expressions (regex) National ID, IBAN, email, phone, IP addresses, credit cards Sub-millisecond processing time, deterministic, zero GPU overhead No context sensitivity; fails on variable spacing, permutations, and personal names
Local token-based NER (spaCy, RoBERTa) Personal names, company names, locations, job titles Contextual understanding, recognizes compound names and addresses Higher latency (5–30 ms per payload), requires CPU/GPU resources, occasional false positives
Local lightweight instruction LLMs Complex indirect identifiers, medical context Very high semantic accuracy on ambiguous data Latency impact (50–200 ms), compute costs, risk of hallucinating entity boundaries

For structured filtering of suspicious patterns and semantic boundaries, the analysis on input validation and output filtering for LLM integrations offers deeper insights into isolating unsafe payload structures and prompt injections. In a robust production environment, we implement a cascade: first regular expressions with checksum validation (such as the 11-test for national ID numbers or ISO 7064 Mod 97-10 for IBAN), followed by a local Named Entity Recognition (NER) step for person and location data.

Anonymization versus pseudonymization

The distinction between permanent anonymization and reversible pseudonymization is decisive for the architecture of the payload pipeline:

When making choices about compliance and legal obligations, read on the hub domain about AI models and privacy under the GDPR how data minimization and data processing agreements are legally defined.

Architecture pattern: the reversible pseudonymization tunnel

When an LLM has to analyze a contract or rewrite customer correspondence, semantic meaning can be lost if all names are reduced to static labels. If three different people are all replaced by [NAAM], the model's reasoning ability about who performs which action gets disrupted.

The solution is a deterministic surrogate mapping pipeline that acts as a proxy between the internal application and the LLM provider:

[Applicatie Client]
       │ (1. Prompt met echte PII: "Jan Jansen betaalt €50 aan Marie Bakker")
       ▼
[PII Gateway Middleware]
       │ ── Identificeer PII via Regex & NER
       │ ── Genereer Vault Key in Redis (TTL: 120s):
       │      "PERSOON_1" -> "Jan Jansen"
       │      "PERSOON_2" -> "Marie Bakker"
       │ (2. Payload: "PERSOON_1 betaalt €50 aan PERSOON_2")
       ▼
[Externe LLM Provider (bijv. OpenAI, Anthropic)]
       │ (3. Response: "De betaling van PERSOON_1 aan PERSOON_2 is verwerkt.")
       ▼
[PII Gateway Middleware]
       │ ── Lees Vault Key uit Redis
       │ ── Vervang "PERSOON_1" met "Jan Jansen"
       │ ── Vervang "PERSOON_2" met "Marie Bakker"
       │ (4. Herstelde Response: "De betaling van Jan Jansen aan Marie Bakker is verwerkt.")
       ▼
[Applicatie Client]

Implementation: Python PII sanitizer & re-hydrator

Below is a complete implementation of a proxy interceptor that performs payload anonymization, hashed token substitution, and response de-anonymization using regular expressions and an abstract lookup store.

import re
import uuid
from typing import Dict, Tuple, Any

class PIISanitizer:
    def __init__(self):
        # Gecompileerde reguliere expressies voor deterministische extractie
        self.iban_pattern = re.compile(
            r'\b[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}\b'
        )
        self.email_pattern = re.compile(
            r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
        )
        self.bsn_pattern = re.compile(r'\b[0-9]{8,9}\b')
        self.phone_pattern = re.compile(
            r'(?:\+|00)?(?:31\s?\(0\)|31)?[\s.-]?[1-9](?:[\s.-]?[0-9]){7,8}\b'
        )

    def _validate_bsn_checksum(self, bsn: str) -> bool:
        """Valideert het BSN via de standaard 11-proef."""
        if len(bsn) == 8:
            bsn = "0" + bsn
        if len(bsn) != 9:
            return False
        factors = [9, 8, 7, 6, 5, 4, 3, 2, -1]
        checksum = sum(int(digit) * factor for digit, factor in zip(bsn, factors))
        return checksum % 11 == 0

    def mask_payload(self, text: str) -> Tuple[str, Dict[str, str]]:
        """
        Vervangt PII door unieke contextuele tokens en levert een omkeertabel.
        """
        mapping: Dict[str, str] = {}
        counter = {"EMAIL": 0, "IBAN": 0, "BSN": 0, "TEL": 0}

        def replace_email(match):
            val = match.group(0)
            counter["EMAIL"] += 1
            token = f"[[EMAIL_ENTITEIT_{counter['EMAIL']}]]"
            mapping[token] = val
            return token

        def replace_iban(match):
            val = match.group(0)
            counter["IBAN"] += 1
            token = f"[[IBAN_REKENING_{counter['IBAN']}]]"
            mapping[token] = val
            return token

        def replace_phone(match):
            val = match.group(0)
            counter["TEL"] += 1
            token = f"[[TEL_NUMMER_{counter['TEL']}]]"
            mapping[token] = val
            return token

        def replace_bsn(match):
            val = match.group(0)
            if self._validate_bsn_checksum(val):
                counter["BSN"] += 1
                token = f"[[BSN_NUMMER_{counter['BSN']}]]"
                mapping[token] = val
                return token
            return val

        # Uitvoeren van regex-substituties
        sanitized_text = self.email_pattern.sub(replace_email, text)
        sanitized_text = self.iban_pattern.sub(replace_iban, sanitized_text)
        sanitized_text = self.phone_pattern.sub(replace_phone, sanitized_text)
        sanitized_text = self.bsn_pattern.sub(replace_bsn, sanitized_text)

        return sanitized_text, mapping

    def restore_payload(self, response_text: str, mapping: Dict[str, str]) -> str:
        """
        Zet de gegenereerde surrogaten deterministisch terug naar de originele PII.
        """
        restored = response_text
        for token, original_value in mapping.items():
            restored = restored.replace(token, original_value)
        return restored

Integration with zero data retention (ZDR)

Anonymization at the application level and contractual data protection at the LLM provider must go hand in hand. Even when payloads are pseudonymized via the method above, remaining sentence structures or subtle metadata may still be indirectly traceable to an individual. It is therefore necessary to enforce zero data retention with external vendors.

To check how to configure your API headers and business contracts so the provider doesn't store prompts, see the overview on configuring zero data retention with LLM APIs. When ZDR is correctly activated and the payload is also pseudonymized, a so-called defense-in-depth architecture arises: should the provider suffer a data breach or activate a faulty configuration, the external logs contain only worthless token surrogates such as [[EMAIL_ENTITEIT_1]].

Logging and observability without PII pollution

A common leak doesn't arise at the model itself, but in your own log aggregation. Developers log complete API requests and responses to troubleshoot errors, causing PII to end up in Elasticsearch, Datadog, or CloudWatch after all. A defensive pipeline therefore strictly separates audit logging and debugging.

See the article on logging for two purposes: audit accountability versus debugging to discover how to retain detailed traceability without keeping personal data in debug environments for a long time. By logging only the masked payload and the computed token hashes in the gateway, observability remains guaranteed without privacy-sensitive fields leaking into general monitoring pipelines.

State management and multi-tenancy isolation

In a SaaS environment with thousands of concurrent requests, temporarily storing the token mappings brings specific infrastructural challenges. If tenant A and tenant B send a request simultaneously, mappings must under no circumstances interfere with or get mixed up with each other.

For software teams designing infrastructures for separated customer data, the guide on building multi-tenant LLM applications explains how to guarantee strict logical isolation across shared models. The following strict design requirements apply to the PII translation layers:

What does PII mitigation cost in production?

Introducing a PII sanitization layer between application and model provider brings measurable trade-offs in terms of latency, cost, and accuracy:

Metric / factor Impact of regex masking Impact of hybrid NER + regex Mitigation strategy
Extra latency (P95) < 2 ms 15 ms – 45 ms Run NER in parallel across text chunks; use optimized ONNX runtime engines.
Token usage model Neutral (+/- 2%) Neutral (+/- 3%) Choose compact tokens (e.g. [[P1]] i.p.v. [[PERSOON_NAAM_VOLLEDIG_IDENTIFICATIE_1]]).
Model reasoning ability Slight degradation with aggressive masking Minimal loss provided semantic tags are retained Use typed tokens (e.g. [[LOCATIE_1]] instead of [MASK]) so the LLM understands the grammatical role.
Infrastructure complexity Low (stateless function) Medium to high (secure state cache + worker pool) Place the sanitizer on the same VPC node as the central reverse proxy to minimize network RTT.

Failure behavior with streamed responses

A complex scenario arises when the external LLM call is streamed to the end user via Server-Sent Events (SSE). The model can smear a token surrogate across multiple consecutive chunks:

Chunk 1: "Het dossier van [["
Chunk 2: "PERSOON"
Chunk 3: "_1]] is goedgekeurd."

Read the technical explanation on streaming responses in LLM APIs to understand how Server-Sent Events are processed and parsed at the network level. If the streaming gateway forwards the chunks unfiltered, the user temporarily sees internal code tags, or de-anonymization fails if a naive string-replace is performed per chunk.

The gateway must therefore implement a sliding window buffer . This buffer holds a small number of tokens (at least the maximum length of a surrogate token, for example 30 characters) and only forwards chunks once it has verified there is no half-formed token in the buffer. As soon as a closed token [[...]] is detected, the original PII is inserted directly before the stream is flushed to the client.

Conclusion and best practices checklist

PII anonymization in production is not a matter of a standalone search-and-replace function, but a systematic part of your middleware architecture. By establishing a structured separation between extraction, substitution, state storage, and re-hydration, data remains protected without the generative capability of the external language model being lost.

Before going live, check the following operational points: