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
- Use
langfuse_otelfor Langfuse v3 or v4, and setLITELLM_OTEL_V2=true. - Set the region host explicitly, and remember the two routes ship different defaults.
- Decide on message redaction before the first request, not after the first leak.
- Pass
trace_idandsession_idin metadata, and expect hashed IDs. - Register custom loggers as instances.
- Plan for a down destination, and decide whether losing logs is acceptable.
- Check which settings need an Enterprise licence.
Common Pitfalls
- Assuming the two Langfuse routes share a region default. They do not.
- Leaving out
LITELLM_OTEL_V2=trueand waiting for traces. The pipeline never starts. - Pointing
langfuse_otelat a non-Langfuse OTLP endpoint. The presets are per-vendor. - Matching
trace_idstrings in your logs against Langfuse. They are normalized or hashed. - Turning on
log_raw_request_responsefor debugging and forgetting it. It is global. - Expecting
redact_user_api_key_infoto cover every destination. Four integrations only. - Handing out
allow_client_message_redaction_opt_outbroadly. It re-enables prompt logging for those keys. - Registering the class instead of the instance. The callback never runs, quietly.
- Assuming an unreachable logging sink is handled. Nothing documents that it is.
Best Book to Read
| Cover | Book | Description | Get it |
|---|---|---|---|
![]() |
Observability Engineering | 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=
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_callbacksandfailure_callbacksdescribed 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 thelangfuse_otelpreset for v3 and v4 for "better span quality, lower latency, and native OpenTelemetry semantics", thelangfusecallback requiringlangfuse>=4.7,<5and exporting through LiteLLM's OpenTelemetry pipeline with the REST client used only for prompt management and credential checks,LANGFUSE_FLUSH_ATandLANGFUSE_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_HOSTdefaulting tohttps://cloud.langfuse.com, thelangfuse_*request headers, and the trace ID normalisation andLangfuse.create_trace_id(seed=...)hashing with v4trace_versionandupdate_trace_keyssemantics: docs.litellm.ai/docs/observability/langfuse_integration[3]LiteLLM, OpenTelemetry v2 — thelangfuse_otelpreset configured withLITELLM_OTEL_V2=trueandcallbacks: ["langfuse_otel"], and the preset table listingLANGFUSE_HOSTorLANGFUSE_OTEL_HOSTwith a default ofhttps://us.cloud.langfuse.comand EU athttps://cloud.langfuse.com: docs.litellm.ai/docs/observability/opentelemetry_v2[4]LiteLLM, Proxy logging —turn_off_message_loggingkeeping spend metadata while dropping messages, the BETAx-litellm-enable-message-redactionrequest header,LiteLLM-Disable-Message-Redactionhonoured only withallow_client_message_redaction_opt_out,redact_user_api_key_infolimited to Langfuse, OpenTelemetry, Logfire and ArizeAI,no-log=Truewithglobal_disable_no_log_param,log_raw_request_responsewriting the raw curl asraw_requestglobally, and thecallbacks:versussuccess_callback:inconsistency: docs.litellm.ai/docs/proxy/logging[5]LiteLLM, Custom callbacks — subclassingCustomLoggerwithlog_pre_api_call,log_post_*and the success and failure hooks, including the async variants: docs.litellm.ai/docs/observability/custom_callback
