In production, observability platforms help you answer three important questions: “What’s happening right now?” “Why is it happening?” and “How can I improve my system?” For applications powered by large language models (LLMs), this goes beyond merely tracking request volume and latency. It also requires tracing individual prompts, API and tool calls, responses, and any relevant context.
Langfuse provides the visibility you need to understand how your LLM-powered applications are performing. This allows you to debug issues, monitor performance, and implement improvements within a unified workflow.
Setup
Langfuse offers both a cloud solution with paid and free plans and an open-source version that can be deployed locally. The managed offering is the easiest to get started with. Just head over to cloud.langfuse.com and set up an instance — you don’t have to install anything. If you prefer the local version, the easiest way to get started is via Docker. Just clone the git repo and start the services:
git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose upIn this case, you need to have Docker installed. This option is best for development and testing. For production-ready options, you can deploy Langfuse to your self-managed Kubernetes cluster or leverage managed clusters on AWS, Azure, or GCP. This provides high availability, but with a more complex setup.
Once installed, you need to create an account for your instance, which you will use to manage your projects. Then, create a new organization and project. Next, create API keys that your applications will use to connect to your instance. These credentials include a public key, a secret key, and the hostname. Here is an example:
LANGFUSE_SECRET_KEY="sk-lf-dfe3ffa9-9514-41b8-9f99-69c9920dc280"
LANGFUSE_PUBLIC_KEY="pk-lf-cff73d3c-1fea-4e25-a52f-76d9d12c6f86"
LANGFUSE_BASE_URL="http://localhost:3000" # or https://cloud.langfuse.com or the DNS of a cloud load balancerThe LANGFUSE_BASE_URL varies based on how you installed Langfuse. You can always generate new keys or delete existing ones in your project settings.
Great. Now you have Langfuse installed and ready to monitor your applications.
Core concepts
Your LLM-powered applications complete various steps before returning a response. First, the user’s input is preprocessed and combined with the necessary context. Then, it is sent to a model provider for processing. The model may call tools to perform various actions before returning the final response. This complete cycle is represented as a trace in Langfuse.
Steps within a trace are represented as observations. They are the core building blocks in Langfuse. There are various observation types, such as events, spans, generation logs, tool calls, guardrails, and more. If you are using a native integration, the observation type is automatically set. We’ll see how you can do it manually with the as_type parameter when creating observations.
Traces can then be grouped into sessions. A session represents a group of traces that form the same interaction. This is useful for multi-turn interactions. This can be done easily by adding a sessionId attribute to observations, as we’ll see shortly.
Besides a sessionId, you can give your traces and observations additional attributes to help you filter and analyze them easily. Here are some useful attributes:
Environment — useful for deployment contexts like
devandprod.Tags — these allow you to categorize traces using labels.
User — track end-users associated with traces.
Metadata — key-value pairs for arbitrary information.
Releases and versions — for change history.
Your first trace
You can use various native integrations for popular libraries such as OpenAI, LangChain, and LlamaIndex. If an integration for your use case does not exist, you can use the Langfuse SDK directly. If you already use OpenTelemetry, a CNCF observability framework, you can send traces to Langfuse at /api/public/otel. Langfuse is built on top of OpenTelemetry, so the integration is seamless.
Here is an example for the OpenAI Python SDK:
# install packages: pip install langfuse openai
from dotenv import load_dotenv
from langfuse.openai import openai # use this instead of from openai import OpenAI
load_dotenv() # load API keys from your environment
response = openai.responses.create(
model="gpt-5.4-mini",
input="Explain the concept of recursion"
)
print(response.output_text)The langfuse.openai wrapper takes care of everything for you automatically. For LangChain/LangGraph, you can use the Langfuse CallbackHandler. Callbacks in LangChain/LangGraph allow you to tap into key LLM and agent execution phases. This helps you perform tasks such as logging, token tracking, and interrupting agent execution.
# install packages: pip install langfuse langchain-openai python-dotenv langchain-core
from langchain_openai import ChatOpenAI
from langfuse.langchain import CallbackHandler
from dotenv import load_dotenv
load_dotenv() # load API keys from your environment
langfuse_handler = CallbackHandler()
llm = ChatOpenAI(model="gpt-5.4-mini")
response = llm.invoke(
input="Explain the concept of recursion.",
config={"callbacks": [langfuse_handler]}
)
print(response.content)Once you run this code, the traces should appear in Langfuse:
Explore the documentation to learn more about using various integrations.
Using Langfuse SDKs
If you are using the Langfuse SDKs, there are three ways to instrument your application. First, you can use a context manager. Context managers automate the setup and cleanup of resources, such as files and connections. Here’s how you can create an observation:
# install packages: pip install langfuse openai python-dotenv
from openai import OpenAI
from dotenv import load_dotenv
import os
from langfuse import get_client
load_dotenv() # load API keys from your environment
langfuse = get_client() # automatically picks up your LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY and LANGFUSE_BASE_URL
client = OpenAI() # automatically picks up your OPENAI_API_KEY and OPENAI_BASE_URL. You must set the base URL if you're using a proxy
with langfuse.start_as_current_observation(name="main", as_type="span") as span:
with langfuse.start_as_current_observation(as_type="generation", name="recursion_explanation") as generation:
user_input = "Explain the concept of recursion"
generation.update(input=user_input)
response = client.responses.create(
model="gpt-5.1",
input=user_input
)
print(response.output_text)
generation.update(output=response.output_text)We are using start_as_current_observation to create observations within a particular context. As you can see, we can create child observations within other observations. This helps us build a hierarchy of observations for various steps in the execution cycle. Once the code finishes execution, you can see the traces in Langfuse:
The second way is to use the observe() decorator for functions. This automatically captures inputs, outputs, and other information about a function without changing the function itself. You can also use the as_type parameter here, depending on what the function does. Each observed function will be recorded as an individual trace.
import os
from dotenv import load_dotenv
from langfuse import get_client, observe
from openai import OpenAI
load_dotenv()
langfuse = get_client()
client = OpenAI()
@observe(name="database-lookup", as_type="span", capture_output=False)
def perform_db_query(query_id: str):
print({"id": query_id, "data": "Sample Database Record"})
@observe(name="generate-text-completion", as_type="generation", capture_input=True, capture_output=True)
def generate_text():
response = client.responses.create(
model="gpt-5.1",
input="Explain the concept of recursion"
)
print(response.output_text)
perform_db_query("123")
generate_text()You can also manually create observations as follows:
from dotenv import load_dotenv
from langfuse import get_client
load_dotenv()
langfuse = get_client()
root = langfuse.start_observation(name="data-processing-pipeline", input="initial-data")
generation = root.start_observation(name="llm-transformation", as_type="generation")
evaluation = generation.start_observation(name="post-processing", as_type="evaluator")
generation.end()
evaluation.end()
root.end()In this case, we must manually nest observations, manage context, and manage their lifecycle using the .end() method. Once you run this code, your traces appear in the Langfuse UI:
Adding attributes
As noted earlier, attributes help you add useful information to your traces and observations. You can set such attributes as follows:
import os
from dotenv import load_dotenv
from langfuse import get_client, observe, propagate_attributes
from openai import OpenAI
load_dotenv()
langfuse = get_client()
client = OpenAI()
@observe()
def process_request():
with propagate_attributes(
session_id="super-awesome-session",
user_id="super-awesome-user",
metadata={"source": "web"},
trace_name="web-request"
):
response = client.responses.create(
model="gpt-5.1",
input="Explain the concept of recursion"
)
print(response.output_text)
process_request()As you can see below, we were able to add attributes to observations using propagate_attributes:
Enriching your traces with attributes like session_id, user_id, and environment provides critical context to your observation hierarchy. A user_id groups logs into clear, individual customer interactions. Meanwhile, the environment tag safely separates development from live production analytics. Once execution finishes, these attributes become searchable filters within your Langfuse dashboard.
Langfuse only tracks and aggregates analytics for end-users or sessions via the custom IDs you provide, but it does not authenticate or create them.
Conclusion
We have examined the features of the Langfuse observability platform. You learned how to set up an instance and instrument your application code. Additionally, we discussed the core concepts of traces, observations, and multi-turn sessions. By utilizing native framework integrations and adding custom attributes and metadata, you gain full control over your telemetry.