JS/TS v5 → v6
The JS/TS SDK v6 targets Langfuse v4 only. It removes the Public API methods and client aliases that were deprecated in v5, records experiments through OpenTelemetry instead of the dataset-run-items endpoint, and sends gzip-compressed span batches with the v4 ingestion header by default.
Most applications that already follow the observations-first model only need to check a few call sites. Start with the migration checklist and use the sections below for details.
JS/TS SDK v6 requires Langfuse Cloud or self-hosted Langfuse v4.55.0 or later. Older servers store JSON-encoded string metadata values with their quotes (see Metadata values are JSON-encoded). If you self-host Langfuse v3, stay on JS/TS SDK v5. See the self-hosted compatibility matrix and Versions & Compatibility.
All packages (@langfuse/core, @langfuse/client, @langfuse/tracing, @langfuse/otel, @langfuse/openai, @langfuse/langchain) are released together. Upgrade them to the same major version:
npm install @langfuse/client@6 @langfuse/tracing@6 @langfuse/otel@6baseUrl, LANGFUSE_BASE_URL, and LANGFUSE_BASEURL keep working as before. You don't need to change them.
Breaking Changes
Node.js 22 and ES2022 required
All packages declare engines.node >=22 and are compiled to ES2022. Upgrade to Node.js 22 or later. If you bundle the SDK for older runtimes, make sure your bundler targets ES2022 or transpiles the SDK.
LangChain v1 required
@langfuse/langchain now has a peer dependency on @langchain/core@^1.0.0 (previously >=0.3.8). Upgrade LangChain before upgrading the integration:
npm install @langfuse/langchain@6 @langchain/core@^1The CallbackHandler API and trace output are unchanged.
Spans are gzip-compressed by default
On Node.js and Bun, LangfuseSpanProcessor now sends gzip-compressed batches. Compression is resolved in this order:
compressionoption onLangfuseSpanProcessorLANGFUSE_OTEL_COMPRESSIONOTEL_EXPORTER_OTLP_TRACES_COMPRESSIONOTEL_EXPORTER_OTLP_COMPRESSIONgzip
Supported values are gzip and none. Environment variables are case-insensitive, and an invalid value falls through to the next setting. Browser and edge runtimes ignore this setting. If a proxy between your application and Langfuse can't handle compressed requests, opt out:
import { LangfuseSpanProcessor } from "@langfuse/otel";
const spanProcessor = new LangfuseSpanProcessor({ compression: "none" });or set LANGFUSE_OTEL_COMPRESSION=none.
v4 ingestion header sent by default
LangfuseSpanProcessor sends x-langfuse-ingestion-version: 4, which enables real-time processing of spans on Langfuse v4. additionalHeaders override default headers case-insensitively, so you can opt out:
import { LangfuseSpanProcessor } from "@langfuse/otel";
const spanProcessor = new LangfuseSpanProcessor({
additionalHeaders: { "x-langfuse-ingestion-version": "3" },
});Deprecated LangfuseClient aliases removed
The top-level aliases on LangfuseClient are removed. Use the managers or the api client instead.
| Removed | Replacement |
|---|---|
langfuse.getPrompt(...) | langfuse.prompt.get(...) |
langfuse.createPrompt(...) | langfuse.prompt.create(...) |
langfuse.updatePrompt(...) | langfuse.prompt.update(...) |
langfuse.getDataset(...) | langfuse.dataset.get(...) |
langfuse.createDatasetItem(...) | langfuse.dataset.createItem(...) |
langfuse.resolveMediaReferences(...) | langfuse.media.resolveReferences(...) |
langfuse.createDataset(...) | langfuse.api.datasets.create(...) |
langfuse.getDatasetItem(id) | langfuse.api.datasetItems.get(id) |
langfuse.fetchMedia(id) | langfuse.api.media.get(id) |
langfuse.fetchTrace(id) | langfuse.api.observations.getMany({ traceId: id, ... }) |
langfuse.fetchTraces(...) | langfuse.api.observations.getMany({ ... }) |
langfuse.fetchObservation(id) | langfuse.api.observations.getMany({ filter: ..., ... }) |
langfuse.fetchObservations(...) | langfuse.api.observations.getMany(...) |
langfuse.fetchSessions(...) | langfuse.api.observations.getMany({ sessionId, ... }) |
langfuse.getDatasetRun(...) | langfuse.api.experiments.list(...) and .listItems(...) |
langfuse.getDatasetRuns(...) | langfuse.api.experiments.list({ datasetId, fromStartTime }) |
v5:
const prompt = await langfuse.getPrompt("support-agent");
const trace = await langfuse.fetchTrace("trace-id");v6:
import { LangfuseClient } from "@langfuse/client";
const langfuse = new LangfuseClient();
const prompt = await langfuse.prompt.get("support-agent");
const observations = await langfuse.api.observations.getMany({
traceId: "trace-id",
fields: "core,basic,io",
});LangfuseClient also exposes getProjectId() and getExperimentUrl(experimentId).
Trace input/output setters removed
setActiveTraceIO(), span.setTraceIO(), createTraceAttributes(), and the LangfuseTraceAttributes type were deprecated in v5 and are removed. Trace input and output now come from the root observation. Set them on the root observation instead.
v5:
import { startActiveObservation, setActiveTraceIO } from "@langfuse/tracing";
await startActiveObservation("request", async () => {
const result = await callLlm(query);
setActiveTraceIO({ input: query, output: result });
});v6:
import { startActiveObservation } from "@langfuse/tracing";
await startActiveObservation("request", async (span) => {
const result = await callLlm(query);
span.update({ input: query, output: result });
// or, inside the active root observation:
// updateActiveObservation({ input: query, output: result });
});setActiveTraceAsPublic() and span.setTraceAsPublic() are unchanged. If you still use trace-level LLM-as-a-judge evaluators, migrate them to observation-level evaluators.
Metadata values are JSON-encoded
v6 JSON-encodes every metadata value before writing it to the span, strings included. This applies to observation metadata (metadata on startObservation(), startActiveObservation(), update(), observe(), and the Vercel AI SDK integration) and to propagated trace metadata from propagateAttributes({ metadata }).
Observation metadata is written as one attribute per top-level key (langfuse.observation.metadata.<key>):
| Value | v5 attribute value | v6 attribute value |
|---|---|---|
"prod" | prod | "prod" |
"3" | 3 | "3" |
3 | 3 | 3 |
true | true | true |
"" | Not written | "" |
{ a: { b: 1 } } | {"a":{"b":1}} | {"a":{"b":1}} |
2n ** 60n | Not written | "1152921504606846976" |
NaN, Infinity | null | "NaN", "Infinity" |
null, undefined | Not written | Not written (debug log) |
propagateAttributes({ metadata }) now accepts Record<string, unknown> instead of Record<string, string>. v5 passed strings through and dropped every other value:
| Value | v5 | v6 |
|---|---|---|
"gpt-4o" | gpt-4o | "gpt-4o" |
42 | Dropped | 42 |
null | Dropped | null |
[1, "a"] | Dropped | [1,"a"] |
{ k: "v" } | Dropped | {"k":"v"} |
12345678901234567890n | Dropped | 12345678901234567890 |
NaN, Infinity, undefined, functions, circular values | Dropped | Dropped with a warning |
The 200-character limit for propagated values applies to the encoded value, including the quotes around strings. Metadata passed to the LangChain CallbackHandler follows the same rules.
How Langfuse v4.55.0 and later reads the encoded values:
- Strings are decoded and stored without quotes, so
"prod"still reads asprod. - Numbers and booleans keep their type instead of becoming strings.
- Nested objects become filterable dotted paths.
{ a: { b: 1 } }can be filtered asa.b = 1instead of matching a JSON string. - Dotted-key collisions: the first value wins. If metadata contains both a literal
"a.b"key and a nested{ a: { b: ... } }, only the first value is kept for thea.bpath.
Self-hosted servers older than v4.55.0 don't decode the values and store string values with their quotes ("prod" including the quotes), so filters on string values stop matching.
Check these places when you upgrade:
- Saved filters, dashboards, and downstream consumers that match the old shape, such as a whole nested object as a JSON string, stop matching for new traces. Update them to the new values or dotted paths. Existing data keeps its stored shape.
- Raw span-attribute readers, such as custom span processors and exporters, now see JSON strings in
langfuse.observation.metadata.*andlangfuse.trace.metadata.*:'"prod"'instead of'prod'. Decode them withJSON.parse(). Themaskfunction ofLangfuseSpanProcessorreceives decoded observation metadata values, and its result is re-encoded. - If you relied on non-string propagated values being dropped, remove them before calling
propagateAttributes.
Request placeholder before requestOptions
Generated API methods that take no query parameters and no body now accept an empty request object before requestOptions. This keeps requestOptions in the same position if the endpoint gains query parameters later. If you pass requestOptions to one of these methods, add {} before it.
v5:
await langfuse.api.datasets.get("qa-set", { timeoutInSeconds: 10 });
await langfuse.api.health.health({ maxRetries: 0 });v6:
await langfuse.api.datasets.get("qa-set", {}, { timeoutInSeconds: 10 });
await langfuse.api.health.health({}, { maxRetries: 0 });TypeScript reports the old call as a compile error. In plain JavaScript, the options would be silently ignored, so check these call sites manually. Calls without requestOptions don't change.
All 41 affected methods
annotationQueues.getQueue(queueId)annotationQueues.getQueueItem(queueId, itemId)annotationQueues.deleteQueueItem(queueId, itemId)blobStorageIntegrations.getBlobStorageIntegrations()blobStorageIntegrations.getBlobStorageIntegrationStatus(id)blobStorageIntegrations.deleteBlobStorageIntegration(id)comments.getById(commentId)datasetItems.get(id)datasetItems.delete(id)datasets.get(datasetName)evaluationRules.get(evaluationRuleId)evaluationRules.delete(evaluationRuleId)evaluators.get(evaluatorId)evaluators.delete(evaluatorId)health.health()legacy.scoreV1.delete(scoreId)llmConnections.delete(id)media.get(mediaId)models.get(id)models.delete(id)organizations.getOrganizationMemberships()organizations.getProjectMemberships(projectId)organizations.getOrganizationProjects()organizations.getOrganizationApiKeys()projects.get()projects.delete(projectId)projects.getApiKeys(projectId)projects.deleteApiKey(projectId, apiKeyId)scim.getServiceProviderConfig()scim.getResourceTypes()scim.getSchemas()scim.getUser(userId)scim.deleteUser(userId)scoreConfigs.getById(configId)trace.delete(traceId)unstable.dashboardWidgets.get(widgetId)unstable.dashboardWidgets.delete(widgetId)unstable.dashboards.get(dashboardId)unstable.dashboards.delete(dashboardId)unstable.dashboards.deletePlacement(dashboardId, placementId)unstable.skills.deleteVersion(skillName, skillVersion)
Dataset item link() removed
item.link() and the LinkDatasetItemFunction type are removed. dataset.items is now a plain DatasetItem[]. Use the experiment runner to run your application on a dataset.
v5:
const dataset = await langfuse.dataset.get("qa-set");
for (const item of dataset.items) {
await startActiveObservation("qa-task", async (span) => {
const output = await myApp(item.input);
span.update({ input: item.input, output });
await item.link(span, "baseline");
});
}v6:
const dataset = await langfuse.dataset.get("qa-set");
const result = await dataset.runExperiment({
name: "baseline",
task: async (item) => myApp(item.input),
evaluators: [accuracy],
});
console.log(result.experimentUrl);See Experiments via SDK for the full runner API.
Experiment runner records experiments through OpenTelemetry
langfuse.experiment.run() and dataset.runExperiment() no longer call POST /api/public/dataset-run-items. Experiment and item attributes are sent on the experiment spans instead. Your experiment code does not need to change, but a few results do:
- Experiment IDs are deterministic. The experiment ID is derived from the project, dataset, and run name, so running again with the same name on the same dataset adds to the same experiment. Experiments on local data get a random ID. A run over items from several datasets gets one experiment per dataset and logs a warning.
- New result field.
result.experimentUrlis the primary link.datasetRunUrlis deprecated, anddatasetRunIdequalsexperimentId. - Run evaluations are always sent, including for local-data runs. They are skipped only when nothing was sampled.
- Dataset versions (
datasetVersion) are recorded on items aslangfuse.experiment.item.version.
Removed Public API methods and types
The generated client under langfuse.api no longer includes the endpoints that Langfuse v4 deprecated. For full replacement guidance, see the SDK method quick reference.
| Removed | Replacement |
|---|---|
api.trace.get(...), api.trace.list(...) | api.observations.getMany({ traceId, ... }) |
api.sessions.* | api.observations.getMany({ sessionId, ... }) |
api.legacy.observationsV1.* | api.observations.getMany(...) |
api.legacy.metricsV1.metrics(...) | api.metrics.metrics(...) |
api.scores.getMany(...), api.scores.getById(...) | api.scoresV3.getManyV3(...) |
api.datasetRunItems.* | api.experiments.listItems(...) or the experiment runner |
api.datasets.getRun(...), .getRuns(...) | api.experiments.list(...) and .listItems(...) |
api.datasets.deleteRun(...) | List experiment items, then api.trace.deleteMultiple({ traceIds }) |
Removed types include the trace, session, v1 observation, dataset run, and dataset run item response types, the ScoreV1 family, and SdkLogEvent.
To delete an experiment's data, page through its items and delete their traces. This also deletes their observations and scores:
import { LangfuseClient } from "@langfuse/client";
const langfuse = new LangfuseClient();
const dataset = await langfuse.dataset.get("qa-set");
const fromStartTime = new Date(Date.now() - 30 * 24 * 3600 * 1000).toISOString();
const traceIds = new Set<string>();
let cursor: string | undefined;
do {
const page = await langfuse.api.experiments.listItems({
datasetId: dataset.id,
experimentName: "baseline",
fromStartTime,
cursor,
});
page.data.forEach((item) => traceIds.add(item.traceId));
cursor = page.meta.cursor ?? undefined;
} while (cursor);
const ids = [...traceIds];
for (let i = 0; i < ids.length; i += 1000) {
await langfuse.api.trace.deleteMultiple({ traceIds: ids.slice(i, i + 1000) });
}See Dataset runs → Experiments for details.
api.scores.create accepts batches
langfuse.api.scores.create() now accepts a single CreateScoreRequest or an array of them, and returns CreateScoresResponse (a union of the single-score and batch responses). Single-score calls keep working, but TypeScript code that reads fields from the response needs to narrow the type first.
v5:
const { id } = await langfuse.api.scores.create({
name: "accuracy",
value: 0.9,
traceId: "trace-id",
});v6:
const response = await langfuse.api.scores.create({
name: "accuracy",
value: 0.9,
traceId: "trace-id",
});
if ("id" in response) {
console.log(response.id);
}langfuse.api.legacy.scoreV1.create(...) keeps the single-score signature and returns CreateScoreResponse. It's a deprecated compatibility alias for api.scores.create.
The SDK's langfuse.score.create(...), langfuse.score.observation(...), and langfuse.score.trace(...) are unchanged.
Other behavior changes
-
Timeouts. v6 uses the same request timeouts as the Python SDK v5:
- 5-second default for prompt fetch, prompt create/update/delete,
media.get, and the media upload API calls. All of these used 60 seconds in v5. Override prompt fetch per call withfetchTimeoutMs. Otherwise, settimeout(in seconds) orLANGFUSE_TIMEOUTonLangfuseClient. Media uploads triggered by tracing use thetimeoutofLangfuseSpanProcessor, and uploads fromdataset.createItemuse the clienttimeout. Atimeoutthat isn't a finite positive number falls back to 5 seconds with a warning. - Media content download in
media.resolveReferences: aborted when no data arrives for 5 seconds. This is an inactivity timeout, not a cap on the total download time; a download that keeps receiving data is never cut off. v5 had no timeout here. - Score ingestion: 5 seconds with one retry on timeout. v5 used 60 seconds plus retries.
- Unchanged: the media blob upload itself has no timeout, and dataset calls still use 60 seconds.
If prompt or media calls regularly take longer than 5 seconds, raise
timeout. - 5-second default for prompt fetch, prompt create/update/delete,
-
Scores never throw. Score methods validate input on the client. Scores with an empty name or a non-finite or non-string value are logged at error level and dropped.
Migration Checklist
- Confirm you're on Langfuse Cloud or self-hosted Langfuse v4.55.0 or later. Stay on JS/TS SDK v5 for Langfuse v3.
- Upgrade to Node.js 22+ and, if you use the LangChain integration,
@langchain/core@^1. - Upgrade all
@langfuse/*packages to v6 together. - If a proxy rejects compressed requests, set
compression: "none"orLANGFUSE_OTEL_COMPRESSION=none. - Search for
getPrompt,createPrompt,updatePrompt,getDataset,fetchTrace,fetchObservation,fetchSessions,getDatasetRun,createDataset,getDatasetItem,createDatasetItem,fetchMedia,resolveMediaReferencesonLangfuseClient→ use the replacements in the alias table. - Search for
setActiveTraceIO/setTraceIO/createTraceAttributes→ setinput/outputon the root observation. - Metadata values are JSON-encoded: update saved filters, dashboards, and consumers that match the old metadata shape, decode metadata attributes in custom span processors and exporters, and check
propagateAttributesandCallbackHandlermetadata for non-string values that were previously dropped. - Search for generated API calls that pass
requestOptionsto a method in the placeholder list → insert{}beforerequestOptions. - Search for
item.link(→ usedataset.runExperiment(). - Search for
datasetRunUrl→ useexperimentUrl. - Search for
api.trace.get,api.trace.list,api.sessions,api.legacy.observationsV1,api.legacy.metricsV1,api.scores.getMany,api.scores.getById,api.datasetRunItems,api.datasets.getRun,api.datasets.deleteRun→ see the SDK method quick reference. - Search for code that reads the response of
api.scores.create(→ narrow the union type. - Prompt, media, and score calls now time out after 5 seconds (previously 60). If they regularly take longer, raise
timeout/LANGFUSE_TIMEOUT, or passfetchTimeoutMstoprompt.get.
Last updated on