Sam Austinon October 27, 202616 min read

LiteLLM Callbacks: Traces in the Wrong Langfuse Region

LiteLLM Callbacks: Traces in the Wrong Langfuse Region
Contents

Figure 1: LiteLLM Callbacks: Traces in the Wrong Langfuse Region — the config is one line; the defaults are the problem

You cannot fix what you cannot see. LiteLLM's callbacks send every request's model, cost, latency and errors to the tool of your choice. Turning one on is a single line. Choosing the right line, and keeping prompts out of logs you do not control, is where the real work sits.

I did not run LiteLLM or Langfuse, since neither is installed here. Settings below come from LiteLLM's docs and Langfuse's own integration page. My script tests the parts you would write yourself, against a local mock sink.

How Callbacks Work

LiteLLM's callback page describes three kinds, "making it easy for you to send data to a particular provider depending on the status of your responses" [1]. The code uses the singular names litellm.input_callback, success_callback and failure_callback, even though the prose uses plurals [1]. You pass each a list of integration names:

import litellm

litellm.success_callback = ["langfuse", "helicone"]
litellm.failure_callback = ["sentry", "langfuse"]

Each integration reads its own environment variables, for example LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY and SENTRY_DSN [1]. The page lists Lunary, Langfuse, LangSmith, Helicone, Traceloop, Athina, Sentry, PostHog, Slack, Arize and PromptLayer, and says that is "not an extensive list" [1].

It does not cover async callbacks, ordering, performance cost or what happens if a provider is unreachable. Those gaps matter more than the list does.

Langfuse: Pick the Route First

Here is the surprise. LiteLLM's Langfuse page recommends the OpenTelemetry route for Langfuse v3 and v4, saying to use "the langfuse_otel preset" for "better span quality, lower latency, and native OpenTelemetry semantics" [2]. Langfuse's own LiteLLM page agrees and documents only that route.

import os, litellm

os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-..."
os.environ["LANGFUSE_SECRET_KEY"] = "sk-..."
os.environ["LITELLM_OTEL_V2"] = "true"      # required for the OTEL v2 pipeline
litellm.callbacks = ["langfuse_otel"]

That LITELLM_OTEL_V2=true line is not optional decoration: LiteLLM's OpenTelemetry v2 page shows it in every preset example, including langfuse_otel [3]. A snippet that sets only the Langfuse keys and the callback name is a snippet that never turns the pipeline on.

On the proxy, add callbacks: ["langfuse_otel"] under litellm_settings, alongside the same environment variables [3].

The older SDK callback still exists: litellm.success_callback = ["langfuse"] and litellm.failure_callback = ["langfuse"] [2]. It needs langfuse>=4.7,<5 [2].

One detail worth knowing before you pick it anyway: the langfuse callback exports traces through LiteLLM's own OpenTelemetry pipeline to Langfuse's OTLP endpoint and uses the SDK's REST client "only for prompt management and credential checks" [2]. The two routes are closer than their names suggest.

If you self-host, the server must be 3.63.0 or newer, and the page warns that OSS v2 servers return 404 on the export route [2]. The page's advice is to upgrade the server before upgrading LiteLLM [2].

Batch behaviour is tunable with LANGFUSE_FLUSH_AT and LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS [2].

The Region Trap

Pay attention to the host, because the default depends on the route.

Route Host variable Documented default
SDK callback langfuse LANGFUSE_HOST https://cloud.langfuse.com, documented as optional [2]
OTEL preset langfuse_otel LANGFUSE_HOST or LANGFUSE_OTEL_HOST https://us.cloud.langfuse.com, with EU at https://cloud.langfuse.com [3]

So the same variable name points at different regions depending on which route you chose. The safest move is to set the host explicitly for your region rather than reasoning about a default you did not pick.

Langfuse's own page also lists Japan and HIPAA hosts. And the OTEL presets are per-vendor — langfuse_otel writes to Langfuse's endpoint [3]. If you want Grafana, Honeycomb or Datadog, use their own presets or a generic OTLP setup, not the Langfuse preset with a swapped URL.

Proxy Config

On the proxy, callbacks live in config.yaml:

litellm_settings:
  callbacks: ["langfuse_otel"]

The proxy page uses callbacks: for OpenTelemetry, GCS and custom loggers, and success_callback: for Langfuse, Deepeval and Lunary. It never explains the difference, so follow each integration's own example [4]. For Langfuse on current versions, that example is callbacks: [3].

Metadata and Trace IDs

You can pass generation_name, trace_id, trace_user_id, session_id, tags and more in metadata. Proxy callers can send headers prefixed langfuse_ [2].

One gotcha, and the docs do name the mechanism this time. Langfuse v4 requires W3C trace IDs of 32 lowercase hex characters. LiteLLM first lowercases your trace_id and strips hyphens, so 01234567-89AB-CDEF-0123-456789ABCDEF becomes 0123456789abcdef0123456789abcdef and is used as-is. Anything that still is not 32 hex characters is deterministically hashed with Langfuse.create_trace_id(seed=<your id>) [2].

The same trace_id always maps to the same Langfuse trace, but the ID visible in Langfuse is the normalized or hashed form, not your original string [2]. My test confirmed the shape of that rule: a UUID-style ID became 32 lowercase hex characters, and order-1234 turned into something unrecognizable.

Two v4 semantics that change dashboards: on a new trace, trace_version takes precedence over version, and update_trace_keys only applies when langfuse_enable_update_trace_keys: true is set [2].

Keep Prompts Out of Logs

Logs are a copy of your data, so decide what goes in them. LiteLLM gives you several controls [4]:

Control Scope What it does
turn_off_message_logging: True Global, under litellm_settings Messages and responses are not logged, but request metadata such as spend still is
x-litellm-enable-message-redaction: true Per request, BETA Turns message redaction on for that request
LiteLLM-Disable-Message-Redaction: true Per request, opt-out Overrides a global redaction, but the proxy honours it only on keys or teams whose metadata sets allow_client_message_redaction_opt_out: true; on any other key "the header is dropped and messages stay redacted"
redact_user_api_key_info: true Global Removes key, user and team identifiers, but only for Langfuse, OpenTelemetry, Logfire and ArizeAI
no-log: True Per call, in the request body Skips logging for that call
global_disable_no_log_param: True Global Removes the no-log option entirely
log_raw_request_response: true Global Attaches the raw curl command as raw_request in Langfuse metadata; no per-request toggle

My test did the same as the global switch: the redacted event kept cost_usd and dropped the messages.

Know the log_raw_request_response row before you enable it. It is global, it has no per-request toggle, and it writes the full provider request — including prompt text — into metadata [4]. That is the opposite of redaction, in the same config file.

Some features, including dynamic callback disabling, GCS and custom-callback API logging, are Enterprise features that need LITELLM_LICENSE. Check the licence box before you plan around them.

Writing a Custom Logger

Subclass CustomLogger and implement hooks such as log_success_event, log_failure_event, async_log_success_event and async_log_failure_event [5].

Register the instance, not the class: callbacks: custom_callbacks.proxy_handler_instance. Naming a class fails config load on newer versions, and older versions started cleanly but never ran the callback.

Request details are under litellm_params["proxy_server_request"]. Also note that standard_logging_object may be missing from intermediate streaming events.

When the Destination Misbehaves

The docs say nothing about what LiteLLM does if a logging provider is down, so I tested the pattern for code you control. A worker thread with a bounded queue kept the request path fast: 500 log calls took about a millisecond in the caller with the sink returning 503, and no exceptions. The trade-off is that 450 events were dropped. If you need an audit trail, a dropped log is a bug, so choose a durable queue instead.

The one failure rule Langfuse's callback does document is oversized batches. When a reverse proxy returns 413, the callback splits the batch in halves and resends, and only a single span that alone exceeds the limit is dropped, with an error log [2]. In my mock, 11 events with one huge item delivered 10 and dropped only the huge one.

A Logging Checklist

  1. Use langfuse_otel for Langfuse v3 or v4, and set LITELLM_OTEL_V2=true.
  2. Set the region host explicitly, and remember the two routes ship different defaults.
  3. Decide on message redaction before the first request, not after the first leak.
  4. Pass trace_id and session_id in metadata, and expect hashed IDs.
  5. Register custom loggers as instances.
  6. Plan for a down destination, and decide whether losing logs is acceptable.
  7. Check which settings need an Enterprise licence.

Common Pitfalls

  1. Assuming the two Langfuse routes share a region default. They do not.
  2. Leaving out LITELLM_OTEL_V2=true and waiting for traces. The pipeline never starts.
  3. Pointing langfuse_otel at a non-Langfuse OTLP endpoint. The presets are per-vendor.
  4. Matching trace_id strings in your logs against Langfuse. They are normalized or hashed.
  5. Turning on log_raw_request_response for debugging and forgetting it. It is global.
  6. Expecting redact_user_api_key_info to cover every destination. Four integrations only.
  7. Handing out allow_client_message_redaction_opt_out broadly. It re-enables prompt logging for those keys.
  8. Registering the class instead of the instance. The callback never runs, quietly.
  9. Assuming an unreachable logging sink is handled. Nothing documents that it is.

Best Book to Read

CoverBookDescriptionGet it
Cover of “Observability Engineering” Observability Engineeringby Charity Majors, Liz Fong-Jones and George Miranda why sampled, structured telemetry beats logs you cannot query, which is the decision hiding behind every callback setting here. View on Amazon

As an Amazon Associate I earn from qualifying purchases. Links on this page are affiliate links, and I may earn a commission at no extra cost to you.

Wrapping Up

LiteLLM callbacks are easy to enable and easy to misconfigure. The expensive mistakes are quiet ones: the wrong Langfuse route or region, prompts logged that should not be, and a class registered where an instance belongs.

Pick the OTEL route, turn on redaction first and send one test request that you check end to end.

If retries and fallbacks come next, the retries guide covers which failures deserve another attempt and the error triage guide covers the codes your logs will show. For the deployment itself, see the Docker production guide.

Frequently Asked Questions

Should I use langfuse or langfuse_otel as a LiteLLM callback?

Use langfuse_otel for Langfuse v3 and v4. LiteLLM's Langfuse page recommends the langfuse_otel preset from the OpenTelemetry v2 guide for better span quality, lower latency and native OpenTelemetry semantics. The older langfuse callback still works but requires the Langfuse Python SDK v4, pinned as langfuse>=4.7,<5, and it exports through LiteLLM's own OpenTelemetry pipeline anyway.

Why do my Langfuse traces land in the wrong region?

Because the default host depends on which route you use. The SDK langfuse callback documents LANGFUSE_HOST as optional and defaulting to https://cloud.langfuse.com, while the OpenTelemetry v2 table lists LANGFUSE_HOST or LANGFUSE_OTEL_HOST with a default of https://us.cloud.langfuse.com and EU as https://cloud.langfuse.com. Set the host explicitly for your region instead of relying on either default.

Why does the trace_id I send not match the one in Langfuse?

Because Langfuse v4 requires W3C trace IDs of 32 lowercase hex characters. LiteLLM lowercases your value and strips hyphens, so a UUID such as 01234567-89AB-CDEF-0123-456789ABCDEF becomes 0123456789abcdef0123456789abcdef and is used as-is. Anything still not 32 hex characters is deterministically hashed with Langfuse.create_trace_id(seed=), so the same input always maps to the same trace even though the visible ID differs.

How do I stop LiteLLM from logging prompts to Langfuse?

Set litellm.turn_off_message_logging, which drops messages and responses while still tracking request metadata such as spend. You can also enable message redaction per request with the x-litellm-enable-message-redaction: true header, which is still in beta, and opt a request back out with LiteLLM-Disable-Message-Redaction: true, which the proxy honours only on keys or teams whose metadata sets allow_client_message_redaction_opt_out: true.

What does redact_user_api_key_info do in LiteLLM?

It removes information about the user API key from logs, such as the hashed token, user_id and team id. It is currently supported for Langfuse, OpenTelemetry, Logfire and ArizeAI logging only, so setting it does not scrub other destinations. Configure it under litellm_settings alongside your callbacks.

What happens when the logging destination is down or rejects a batch?

LiteLLM's docs do not describe behaviour for an unreachable destination, so you have to decide whether losing logs is acceptable and buffer accordingly; a bounded in-memory queue keeps the request path fast but drops events when it fills. For oversized batches the Langfuse callback does document behaviour: when a reverse proxy answers 413, the callback splits the batch in halves and resends, dropping only a single span that alone exceeds the limit, with an error logged.

Sources

  • [1] LiteLLM, Callbacks — input_callbacks, success_callbacks and failure_callbacks described in prose with singular names in code, the integration list of Lunary, Langfuse, LangSmith, Helicone, Traceloop, Athina, Sentry, PostHog, Slack, Arize and PromptLayer, "not an extensive list", and the env-var-per-integration pattern: docs.litellm.ai/docs/observability/callbacks
  • [2] LiteLLM, Langfuse — the recommendation of the langfuse_otel preset for v3 and v4 for "better span quality, lower latency, and native OpenTelemetry semantics", the langfuse callback requiring langfuse>=4.7,<5 and exporting through LiteLLM's OpenTelemetry pipeline with the REST client used only for prompt management and credential checks, LANGFUSE_FLUSH_AT and LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS, the 3.63.0 self-host floor with OSS v2 servers returning 404 on the export route, the 413 batch-splitting behaviour, LANGFUSE_HOST defaulting to https://cloud.langfuse.com, the langfuse_* request headers, and the trace ID normalisation and Langfuse.create_trace_id(seed=...) hashing with v4 trace_version and update_trace_keys semantics: docs.litellm.ai/docs/observability/langfuse_integration
  • [3] LiteLLM, OpenTelemetry v2 — the langfuse_otel preset configured with LITELLM_OTEL_V2=true and callbacks: ["langfuse_otel"], and the preset table listing LANGFUSE_HOST or LANGFUSE_OTEL_HOST with a default of https://us.cloud.langfuse.com and EU at https://cloud.langfuse.com: docs.litellm.ai/docs/observability/opentelemetry_v2
  • [4] LiteLLM, Proxy logging — turn_off_message_logging keeping spend metadata while dropping messages, the BETA x-litellm-enable-message-redaction request header, LiteLLM-Disable-Message-Redaction honoured only with allow_client_message_redaction_opt_out, redact_user_api_key_info limited to Langfuse, OpenTelemetry, Logfire and ArizeAI, no-log=True with global_disable_no_log_param, log_raw_request_response writing the raw curl as raw_request globally, and the callbacks: versus success_callback: inconsistency: docs.litellm.ai/docs/proxy/logging
  • [5] LiteLLM, Custom callbacks — subclassing CustomLogger with log_pre_api_call, log_post_* and the success and failure hooks, including the async variants: docs.litellm.ai/docs/observability/custom_callback

What are You Looking For?

esc