Your Langfuse Traces Are Customer Data. Mask Them Before They Leave.

Sending Langfuse traces is an egress of customer ticket text; production is masking, flush, and a hosting choice you make on purpose.

Rick Hightower

Cover image for “Your Langfuse Traces Are Customer Data. Mask Them Before They Leave.” by Rick Hightower

Production Langfuse is masking, sampling, flush, retention, and RBAC, then a hosting choice. Cloud is the default. Compose is for trying. Helm is for operating.

You shipped tracing to see tickets. You also started sending emails and order ids to another backend, and you have not decided that on purpose.

In this article: You will learn how to treat Langfuse as a production system, not just a trace viewer. We cover mask_otel_spans at export, client-side sampling, flush and shutdown so short-lived jobs do not drop batches, retention and RBAC, then the hosting choice: Langfuse Cloud by default, Docker Compose to try the architecture, Helm or Terraform to run it.

Harbor Desk is a support-ticket assistant. It classifies a ticket, retrieves a policy snippet, and drafts a reply. It traces, versions prompts, scores drafts, runs experiments, and charts cost. It also sends customer ticket text to an observability backend. Soaked life jackets. Order ids. Email addresses.

That last sentence is not a telemetry footnote. It is a data-handling decision. The exporter is an egress path, because that is what an exporter is.

Langfuse production is the hygiene you apply before you treat that path as shipped: mask at export, sample when volume hurts, flush on shutdown, set retention and RBAC, then pick a host.

This article is that hygiene.

A customer ticket becomes OTEL spans. With mask_otel_spans at export, redacted spans reach Langfuse. Without the hook, emails and order ids leave the process.

Mask before the span leaves the process

Masking redacts sensitive fields before the client sends trace data. The Python SDK has two hooks. For new setups, prefer mask_otel_spans.

  • mask_otel_spans, the recommended hook, runs at export. It runs after Langfuse decides which OpenTelemetry spans this client should send, and after media handling. It sees Langfuse SDK spans and third-party instrumentations: the OpenAI drop-in, the Agent SDK instrumentor, and LangChain. You return sparse patches for the spans that should change.
  • mask, the legacy hook, runs synchronously when the Langfuse SDK creates attributes. It only sees data you set through Langfuse APIs (start_observation, update, and set_trace_io). It will not see an OpenInference span.

The function is the redact policy. The constructor call is what installs it. Returning None is the no-op path when nothing in the batch needs a patch.

from typing import Optional

from langfuse import Langfuse
from langfuse.types import (
    MaskOtelSpansParams,
    MaskOtelSpansResult,
    OtelSpanPatch,
)


def mask_otel_spans(*, params: MaskOtelSpansParams) -> Optional[MaskOtelSpansResult]:  # ①
    patches = {}
    for identifier, span in params.spans.items():  # ②
        # Redact attributes you do not want in Langfuse.
        # Example: drop raw prompt payloads from a given instrumentation scope.
        if span.instrumentation_scope_name == "openai":  # ③
            patches[identifier] = OtelSpanPatch(delete_attributes=())  # ④
    if not patches:
        return None  # ⑤
    return MaskOtelSpansResult(patches=patches)


Langfuse(mask_otel_spans=mask_otel_spans)  # ⑥

① The hook is a keyword-only callback. Its return type is optional so a no-op batch can skip a result object. ② The exporter hands over the spans it is about to send, keyed by identifier. ③ The example policy matches on instrumentation scope, so OpenAI drop-in spans can be treated differently from the rest of the batch. ④ The patch is sparse: it names a change for one span instead of rebuilding the span. ⑤ Returning None when the patch map is empty leaves the batch unchanged. ⑥ The client constructor is the install step. Without this argument, the function never runs.

Note: The full extracted listing at code/langfuse/part-11-production-masking-self-hosting/listings/01-mask-otel-spans.py shows the runnable form.

Harbor Desk should redact emails and order ids in ticket text if those must not land in the backend. Write that as a transform on the attributes you actually export. Test it against one soaked-life-jacket trace before you call it done.

Harbor Desk records spans and returns a reply without waiting. The SDK queue batches, mask_otel_spans patches the batch at export, and shutdown on the worker hook sends the last batch.

Sample, flush, and do not block the request

Sampling is client-side. LANGFUSE_SAMPLE_RATE or sample_rate / sampleRate is a float from 0 to 1. Default is 1: everything. 0.2 keeps 20% of traces. Sampling is per trace: if the client keeps the trace, it keeps every observation and score on it. Use this when volume is the problem, not when you are still debugging Harbor Desk.

Queuing and batching stay on. flush_at / flushAt and flush_interval / flushInterval control the batch. Long-running servers can leave the defaults. Short-lived jobs must flush() / forceFlush() / shutdown() before the process dies. That includes a nightly export job, a Lambda, and CI. flush() logs and retries on network errors. It does not throw.

In production: call shutdown() from the worker's shutdown hook, not from inside handle_ticket.

A worker records spans into a queue, exports on an interval, and drains through shutdown. If the process dies first, the last batch is lost.

Retention, RBAC, environment

Data retention is per project: a number of days, with a minimum of 3. Owners and admins set it in Project Settings, or via the projects API. Without a policy, self-hosted Langfuse keeps event data indefinitely. Cloud plans have an access window. A nightly job deletes traces, observations, scores, and media older than that window.

Cloud access windows, as a flat list:

  • Hobby: 30 days
  • Core: 90 days
  • Pro and Enterprise: 3 years

RBAC is users, organizations, projects, and roles. Users get an org role by default and can get a tighter project role. API keys belong to a project, not to a user. Harbor Desk's ingest keys are project keys. Do not hand the production secret to the notebook that runs local experiments.

Environment stays in the deploy: LANGFUSE_TRACING_ENVIRONMENT=production. Keep staging traces out of the production board and out of the production judges.

Cloud vs self-host

Langfuse Cloud is the same product. Langfuse operates it for you. It is the fastest start and the default for Harbor Desk unless you have a reason.

Langfuse self-hosting runs that same architecture. Two application containers handle the work: Web serves the UI and APIs, and Worker processes events. Then come Postgres for OLTP, ClickHouse for traces, observations, and scores, Redis or Valkey for cache and queue, and S3 or compatible blob storage for raw events, multimodal, and exports. An LLM gateway is optional, for the playground and evals. Tracing itself is captured client-side and does not need the LLM API.

Self-hosted Langfuse: clients hit Web and Worker, and both talk to Postgres, ClickHouse, Redis or Valkey, and S3-compatible blob storage.

Docker Compose, via docker-compose.yml in the Langfuse repo, is the simplest way to run it locally or on a VM. It is for trying and for low scale. It does not give you high availability, scaling, or backups. Coming from Compose v2, use the v2-to-v3 upgrade guide.

Production-scale is Kubernetes with Helm, or the AWS, Azure, or GCP Terraform guides, or Railway. If you are on a cloud VM with Compose and you care about the tickets, you are already past the recommended shape.

Where to run Langfuse: Cloud is the default, Compose is how you try, Helm or Terraform is how you operate, and a reachable box needs hardening.

Hardening a self-hosted instance: AUTH_DISABLE_SIGNUP=true if the box is reachable, AUTH_DOMAINS_WITH_SSO_ENFORCEMENT for SSO-only domains, and email verification if you keep password signup. Email verification needs transactional email. Restrict outbound URLs that user-configured integrations may hit. Tighten admin surfaces. See Authentication and SSO, Networking, Encryption, and Deployment Strategies for the rest.

SDK to server. Python v3/v4 and JS v4/v5 want self-hosted server >= 3.63.0. Observations API v2 and Metrics API v2 want Langfuse v4. Cloud always meets those minimums. Check the compatibility matrix before you pin an old chart.

Some add-on features need a license key. Tracing, prompts, scores, and experiments do not wait on that for the Harbor Desk path.

Do this today

  • Label production. Set LANGFUSE_TRACING_ENVIRONMENT=production on the production worker, and keep staging out of that project.
  • Install the mask. Add a mask_otel_spans hook that redacts emails and order ids, then test it against one real ticket trace.
  • Flush on the way out. Call shutdown() from the worker shutdown hook, not from handle_ticket. If you have a Lambda or CI job, add flush() before exit.
  • Stay on Cloud unless you have a reason. Try Compose if you need to see the architecture. Operate with Helm or Terraform if you must run it.
  • Keep secrets scoped. Production ingest keys stay off the notebook that runs local experiments.

Treat the exporter like an egress path

Production Langfuse is masking, sampling, flush, retention, and RBAC, then a hosting choice. Cloud is the default. Compose is how you try the architecture. Helm or Terraform is how you run it.

You started with one import and a classify call. You end with a ticket assistant you can see, judge, experiment on, and operate. Harbor Desk's tickets are customer data. Treat the exporter like an egress path, because it is one.

If you want to go further: the OpenTelemetry-only path, since you already emit OTEL, and the SDK upgrade paths, Python v3 to v4 and JS v4 to v5. The main arc stops here: a wrapped loop, a named tree, a prompt you can change, a score you can trust, and a backend you chose on purpose.