Every team that builds AI agents eventually hits the same wall: the hard part is not writing the agent, it is seeing what the agent actually does. At Brainforge we run a platform where Mastra agents, CopilotKit chat routes, voice interview traces, and a fleet of Cloudflare workers all report into Langfuse. When Langfuse announced that Cloud would become v4-only on November 16, 2026, we had a decision to make. Migrate early on our own schedule, or get forced over later under pressure.
We migrated in August 2026, three months ahead of the deadline. This post is the story of that migration: what we moved, the OpenTelemetry (OTEL) problems that actually cost us time, and what we would do differently if we started over.
Key Takeaways
- Langfuse Cloud removes legacy trace, observation, and scores GET endpoints on Nov 16, 2026. Projects that still read them with v3 SDKs or the old
/api/public/tracesendpoints must move before then. - v4 is OTEL-native. Tracing goes through the OpenTelemetry pipeline, and the fastest path is the scoped SDK packages (
@langfuse/tracing,@langfuse/otel) plus thex-langfuse-ingestion-version: 4header for real-time ingestion. - Coexisting OTEL providers is the real fight. We run HyperDX and Langfuse in one process, and the two pull against each other on the global tracer provider and conflicting OpenTelemetry version lines.
- Read endpoints moved to v2/v3. Observations reads became
/api/public/v2/observationswith cursor pagination; scores reads became/api/public/v3/scores. - Keep score writes on the ingestion path. Score creation via
POST /api/public/scoresis not deprecated, and you should leave it alone.
Why we moved early
Langfuse runs v3 and v4 side by side until the cutover, and its v4 announcement is clear about the direction: a single denormalized observations table replaces read-time joins between traces and observations, ingestion writes each observation once, and queries scan less data. That is why v4 loads tables in milliseconds and runs large dashboards at least ten times faster. It is also why the old API shapes disappear. Deprecated trace and observation reads stop working after the cutover, and the v3 SDK quietly sends updates the new data model no longer wants.
Our instinct was to treat the date as a forcing function rather than a deadline. A forced migration is the worst kind: no room to test, no time to compare behavior, and a team that ships fixes at 11pm because something broke in production. By moving early we could migrate in small PRs, verify against live projects, and keep the v3 path running until each new path was proven.
What the migration touched
The work split into three parts, shipped as separate pull requests.
1. The last v3 SDK consumers moved to @langfuse/tracing
Our case-study voice interview routes were the last code still using the legacy langfuse.trace() and generation.end() API. We rebuilt them on the v4 OTEL API: a deterministic createTraceId(seed) plus startObservation() calls that share a trace ID, so observations group into a trace without a manual root handle. Then we dropped langfuse@^3.38.6 from the dependency tree entirely. Voice traces now land through the OTEL endpoint with the v4 ingestion header.
2. REST reads moved to the v2 and v3 endpoints
Our tooling reads a lot: the CLI that hydrates traces for latency, severity, and cost, scheduled scorecard jobs, the Slack assistant digest, and our eval gate. Every one of those reads went from the deprecated list endpoints to /api/public/v2/observations with cursor pagination and root-observation reconstruction, and scores reads moved to /api/public/v3/scores. The discipline we kept: score writes stayed on the ingestion path, because POST /api/public/scores is not deprecated and bundling it elsewhere is a trap.
3. Mastra needed a core upgrade to reach v4
Mastra agent traces still flowed through a v3 exporter. The v4-capable @mastra/langfuse requires @mastra/core ≥1.16, and we were pinned to 1.8.0. Trying to run it crashed at startup with DEFAULT_BLOCKED_LABELS is not iterable. The fix was a cross-cutting upgrade of the whole @mastra/* set to the core 1.61 line and an aligned OpenTelemetry tree, which let Mastra agents export through @langfuse/otel on the v4 OTEL path. No application code changes were needed; the agents compiled clean once the runtime matched.
The OTEL gotchas that cost us the most
We keep an internal document of every integration problem we paid for, and it now runs to eighteen entries. The v4 migration produced most of the expensive ones. These are the five worth knowing before you start.
The .end() signature changed silently
In the v3 SDK, generation.end({ output, usage }) accepted attributes. In v4, .end() only takes an end time. Attributes must go through .update() before you call .end(). TypeScript flags it if you read the error, but it is easy to power through the type mismatch and end up with spans that never carry their output.
Streaming responses kill the root span before the agent finishes
Our chat route returns a streaming response before the agent has done its work. The old pattern ended the root span when the route handler returned, so the agent finished in the background, tried to write its output, and the update silently dropped. The trace showed the user input but an empty parent output while the child generation underneath had the full reply. The fix was to stop ending the span on handler return: tee the response body, send one branch to the browser so the UI still streams, drain the other branch in the background, and only end the root span after the drain completes.
Two OTEL providers in one process
We run HyperDX and Langfuse in the same Next.js process, and both want to own the global tracer provider. The second registration silently no-ops. Worse, @langfuse/otel expects the OpenTelemetry 2.x line while HyperDX pins the 0.x line, so the span processor never actually wires up. On the manual runtime path our answer was to skip @langfuse/otel and point a plain exporter-trace-otlp-http at the Langfuse OTLP endpoint with basic auth and the v4 ingestion header. On the agent path, aligning the whole OpenTelemetry tree to the 2.x line, as part of the Mastra upgrade above, is what let @langfuse/otel's span processor flush correctly. The general rule: when two providers fight in one process, decide ownership explicitly and give each provider its own pipeline.
Do not trust NodeSDK to install the context manager
Every @langfuse/tracing call warned that there was no active span in context, even after the SDK started. The reliable path was explicit: enable an AsyncHooksContextManager, set it as the global context manager, build a BasicTracerProvider with our processors, and register it as the global tracer provider. No black box.
Filter the auto-spans at the processor boundary
Once a global tracer is installed, Next.js emits a span for every HTTP request, route handler, page render, and fetch. Without filtering, your trace list fills with server noise. We forward only spans whose scope name is the Langfuse SDK, and we check both property names, instrumentationLibrary on the 1.x line and instrumentationScope on 2.x. Checking only one silently dropped everything.
What we would do differently
- Decide OTEL stack ownership before instrumenting anything. Most of our pain came from two vendors sharing one global provider. Pick the primary tracer early, and design the coexistence contract before you write the first span.
- Keep one package per concern. We ended up with several Langfuse-shaped packages in the tree. Scope them by job: tracing, prompt management, and OTEL export are different concerns and mixing them is a footgun.
- Migrate reads before the deadline, deliberately. Cursor pagination on the v2 observations endpoint behaves differently from the old list API. Do it when you have time to diff behavior, not when a deprecation forces you.
- Write down the gotchas as you hit them. The internal doc we kept turned an eighteen-problem migration into a repeatable playbook for the next one.
Migration work is rarely glamorous, but doing it early meant we shipped it in calm, reviewed pieces instead of under a deadline. The v4 data model is measurably faster to query, and the OTEL-native SDK finally makes observability something you can compose rather than bolt on.
If your team is building agents and trying to see what they actually cost and do, Brainforge builds and runs AI observability systems like the one described here. Book a conversation and we can look at your stack together.



