Langfuse v4: up to 165× faster · Read more
ResourcesMigrate from Arize AX to Langfuse

Migrate from Arize AX to Langfuse

This guide walks through migrating LLM observability from Arize AX to Langfuse: live tracing first (usually a same-day change), then datasets, experiments, and evaluators.

Dynatrace announced a definitive agreement to acquire Arize (August 2026). Arize AX and Phoenix continue to operate as they do today. This guide is for teams on Arize AX. If you self-host Phoenix, use Migrate from Arize Phoenix instead. For a product comparison, see Langfuse vs. Arize AX / Phoenix.

TL;DR:

  • Keep your OpenInference instrumentors.
  • Switch the AX exporter to Langfuse.
  • Copy datasets via the AX API into Langfuse dataset items.
  • Recreate judges and re-run experiments; do not import historical traces or old experiment scores.

Want help cutting over? Talk to us about Cloud (EU, US, Japan; HIPAA on Pro+) or self-host.

Why teams migrate

Teams tend to evaluate an AX-to-Langfuse move for a few recurring reasons:

  • Hosting and licensing model. Arize AX is proprietary SaaS; self-hosting AX is an Enterprise option. Langfuse's core is MIT-licensed, and self-hosting runs the same core product as Langfuse Cloud (EU, US, Japan; HIPAA on Pro+ with a signed BAA). Enterprise governance modules need an Enterprise license when self-hosted.
  • Data plane and portability. AX Cloud stores telemetry in adb, a proprietary OLAP engine. Lakehouse sync via Data Fabric is an Enterprise feature and, as of August 2026, waitlisted. Langfuse is API-first on every plan, runs on ClickHouse, and can export to blob storage.
  • One product for the production loop. AX covers tracing, datasets, experiments, prompt hub, and online evals. Adjacent capabilities such as Signal, Alyx, and Data Fabric sit on separate product surfaces and plans. Langfuse keeps that loop on one MIT codebase: production traces feed datasets and experiments, managed evaluators can run on live traffic, and custom dashboards and alerts sit on the same data model.

Arize AX remains a capable production platform, and if it serves your team well, there is no urgency to move. This guide is for teams that have decided to consolidate on Langfuse.

Concept mapping

Arize AXLangfuseNotes
SpaceOrganizationAX space is the tenant; copy Space ID only for the export script
Tracing projectProjectCreated on first spans in AX. In Langfuse, the project is the tenant; API keys select it
Traces / spans (OpenInference)Traces / observationsKeep CHAIN / LLM / TOOL kinds
Datasets + examplesDatasetsCustom columns become metadata
ExperimentsExperiments / dataset runsRe-run; do not import AX run JSON as scores
Evaluator Hub / online evalsLLM-as-a-judgeRecreate rubrics; attach to traces or experiments
PlaygroundPlaygroundReplay traced generations
Prompt hubPrompt managementLabels instead of AX environments

Supported data types

DataMove?Path
Live OpenInference / OTel tracesYesSwitch the AX exporter to Langfuse; keep instrumentors and manual CHAIN/TOOL spans
Dataset examplesYesAX dataset examples API → Langfuse dataset items
Experiment codeYesPoint the experiment runner at the copied dataset
LLM-as-a-judge rubricsRecreateNew Langfuse evaluators
Prompts in AX prompt hubRecreateFetch from AX, create Langfuse prompts with labels
Historical spans (export / parquet)Usually noNo turn-key bulk import; re-run showcase requests if you need a few
AX experiment scores / extra fieldsNoRe-run judges in Langfuse

Step 1: Keep OpenInference, switch the exporter

Keep the OpenInference instrumentor. Change the exporter from AX to Langfuse.

Set LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_BASE_URL (https://cloud.langfuse.com for EU; see Get started for US, Japan, HIPAA, and self-hosted). Confirm LLM spans show as generations with input, output, and token/cost. Do not point leftover ARIZE_* env vars at Langfuse. Langfuse routes by API key, not AX project name.

AX apps send traces with arize.otel.register() from package arize-otel (not arize). Call get_client() before .instrument(). The AX Get started page shows ARIZE_OTLP_ENDPOINT as HTTPS (for example https://otlp.eu-west-1a.arize.com/v1 on EU). arize-otel still exports gRPC to that host by default. Langfuse accepts OTLP over HTTP with Basic auth.

# Before
import os
from arize.otel import register
from openinference.instrumentation.anthropic import AnthropicInstrumentor  # or OpenAIInstrumentor, ...

tracer_provider = register(
    space_id=os.environ["ARIZE_SPACE_ID"],
    api_key=os.environ["ARIZE_API_KEY"],
    project_name=os.environ["ARIZE_PROJECT_NAME"],
    endpoint=os.environ["ARIZE_OTLP_ENDPOINT"],  # copy from the AX Get started page
)
AnthropicInstrumentor().instrument(tracer_provider=tracer_provider)

# After
from langfuse import get_client
from openinference.instrumentation.anthropic import AnthropicInstrumentor

get_client()
AnthropicInstrumentor().instrument()

arize-otel is tracing (0.x). arize is datasets and experiments (v8 needs Python 3.10+). Installing arize alone does not send traces.

AX JS/TS apps send OpenInference spans over OTLP. There is no register() helper. @arizeai/ax-client is the platform API (datasets, experiments), not a tracer.

The AX Get started value is ARIZE_OTLP_ENDPOINT ending in /v1. The JS exporter needs /v1/traces. Use HTTP/OTLP with headers arize-space-id and arize-api-key. LangfuseSpanProcessor reads the Langfuse env vars. JS OpenInference scopes look like @arizeai/openinference-instrumentation-anthropic; the default Langfuse filter matches Python-style openinference.* names, so allowlist the @arizeai/openinference prefix.

// Before
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SEMRESATTRS_PROJECT_NAME } from "@arizeai/openinference-semantic-conventions";
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
import Anthropic from "@anthropic-ai/sdk";

const instrumentation = new AnthropicInstrumentation();
instrumentation.manuallyInstrument(Anthropic);

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    [SEMRESATTRS_PROJECT_NAME]: process.env.ARIZE_PROJECT_NAME!,
  }),
  spanProcessors: [
    new SimpleSpanProcessor(
      new OTLPTraceExporter({
        // Get started value is .../v1; the JS exporter needs .../v1/traces
        url: `${process.env.ARIZE_OTLP_ENDPOINT}/traces`,
        headers: {
          "arize-space-id": process.env.ARIZE_SPACE_ID!,
          "arize-api-key": process.env.ARIZE_API_KEY!,
        },
      }),
    ),
  ],
});
provider.register();

// After
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  LangfuseSpanProcessor,
  isDefaultExportSpan,
  type ShouldExportSpan,
} from "@langfuse/otel";
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
import Anthropic from "@anthropic-ai/sdk";

const afterInstrumentation = new AnthropicInstrumentation();
afterInstrumentation.manuallyInstrument(Anthropic);

const shouldExportSpan: ShouldExportSpan = ({ otelSpan }) =>
  isDefaultExportSpan(otelSpan) ||
  otelSpan.instrumentationScope.name.startsWith("@arizeai/openinference");

const sdk = new NodeSDK({
  spanProcessors: [new LangfuseSpanProcessor({ shouldExportSpan })],
  instrumentations: [afterInstrumentation],
});
sdk.start();

Worked example: Tracing using the OpenInference SDK.

AX tracing notes
  • Manual CHAIN and TOOL spans stay. OpenInference captures the provider HTTP call. It does not wrap your tool execution or agent loop. Production AX apps add a CHAIN span around the loop and TOOL spans around each tool. Keep those attributes (openinference.span.kind) when you cut over.
  • Region. Match AX to the app hostname you log into (app.arize.com, app.eu-west-1a.arize.com, app.ca-central-1a.arize.com). Langfuse region is independent: pick EU / US / Japan / HIPAA / self-host with LANGFUSE_BASE_URL.
  • In the application process, unset ARIZE_SPACE_ID, ARIZE_API_KEY, and ARIZE_OTLP_ENDPOINT after cutover.
  • If you already export with a generic OTLP span exporter, point it at Langfuse:
OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel"  # EU
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ${AUTH_STRING},x-langfuse-ingestion-version=4"
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"

AUTH_STRING is echo -n "pk-lf-...:sk-lf-..." | base64.

If Langfuse is missing LLM, chain, or tool spans

The default Langfuse span filter is LLM-focused. Custom CHAIN/TOOL tracers, and some OpenInference scopes, need an allowlist. If AX showed an agent loop and tools around the LLM call, add that tracer.

Python SDK v4 drops extra CHAIN/TOOL spans from a custom tracer unless you allowlist it. OpenInference LLM spans still arrive without this. SDK v3 on Python 3.9 still exports those spans by default; do not treat that as the v4 cutover.

from langfuse import Langfuse, get_client
from langfuse.span_filter import is_default_export_span
from openinference.instrumentation.anthropic import AnthropicInstrumentor

Langfuse(
    should_export_span=lambda span: (
        is_default_export_span(span)
        or (
            span.instrumentation_scope is not None
            and span.instrumentation_scope.name == "my-app"
        )
    )
)
get_client()
AnthropicInstrumentor().instrument()

Alternatively, create the agent and tool spans with the Langfuse SDK so they are in the langfuse-sdk scope.

JS/TS SDK v5 drops extra CHAIN/TOOL spans from a custom tracer unless you allowlist it. Also allowlist @arizeai/openinference or generations from the JS OpenInference instrumentors will not appear.

import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  LangfuseSpanProcessor,
  isDefaultExportSpan,
  type ShouldExportSpan,
} from "@langfuse/otel";

const shouldExportSpan: ShouldExportSpan = ({ otelSpan }) =>
  isDefaultExportSpan(otelSpan) ||
  otelSpan.instrumentationScope.name === "my-app" ||
  otelSpan.instrumentationScope.name.startsWith("@arizeai/openinference");

const sdk = new NodeSDK({
  spanProcessors: [new LangfuseSpanProcessor({ shouldExportSpan })],
});
sdk.start();

Alternatively, create the agent and tool spans with the Langfuse SDK so they are in the langfuse-sdk scope.

Step 2: Export and recreate datasets

List examples from AX and insert them as Langfuse dataset items. User-defined columns are allowed on create. Reuse the AX example id as the Langfuse item id so a retry upserts instead of duplicating. Do not put AX created_at / updated_at on the Langfuse item; those are server-managed on AX and Langfuse has no slot for them.

Regional API base: https://api.arize.com (US) or https://api.eu-west-1a.arize.com (EU). Authenticate with Authorization: Bearer <ARIZE_API_KEY>. This copies the latest version's examples, not full AX version history. Extra columns (scenario, tags, split) belong in metadata. Confirm item counts match after import.

Paginate with pagination.next_cursor while has_more is true. On Python 3.10+ you can use SDK v8 (from arize import ArizeClient) with client.datasets.list_examples(dataset=..., space=..., all=True). Use REST when pip install arize is not v8 (common on Python 3.9).

import os
import requests
from urllib.parse import quote
from langfuse import get_client

AX_BASE = os.environ["ARIZE_API_BASE"]  # e.g. https://api.eu-west-1a.arize.com
AX_KEY = os.environ["ARIZE_API_KEY"]
DATASET_ID = os.environ["ARIZE_DATASET_ID"]
DATASET_NAME = "support-golden"

examples = []
cursor = None
while True:
    params = {"limit": 500}
    if cursor:
        params["cursor"] = cursor
    payload = requests.get(
        f"{AX_BASE}/v2/datasets/{quote(DATASET_ID, safe='')}/examples",
        headers={"Authorization": f"Bearer {AX_KEY}", "Accept": "application/json"},
        params=params,
        timeout=30,
    ).json()
    examples.extend(payload.get("examples") or [])
    pagination = payload.get("pagination") or {}
    if not pagination.get("has_more"):
        break
    cursor = pagination.get("next_cursor")
    if not cursor:
        break

langfuse = get_client()
langfuse.create_dataset(name=DATASET_NAME)

for row in examples:
    langfuse.create_dataset_item(
        id=row["id"],  # stable retry key in Langfuse; this is the AX example id
        dataset_name=DATASET_NAME,
        input=row.get("input") or row.get("query") or row.get("question") or row,
        expected_output=row.get("expected_output")
        or row.get("output")
        or row.get("answer"),
        metadata={
            k: v
            for k, v in row.items()
            if k
            not in {
                "id",
                "created_at",
                "updated_at",
                "input",
                "expected_output",
                "query",
                "question",
                "output",
                "answer",
                "annotations",
            }
        },
    )

@arizeai/ax-client wraps the same REST API (listDatasetExamples); the package is beta. Set ARIZE_BASE_URL for your region (EU: https://api.eu-west-1a.arize.com). Pagination is camelCase (hasMore, nextCursor). Create the Langfuse dataset with api.datasets.create, then insert items with dataset.createItem.

import { listDatasetExamples } from "@arizeai/ax-client";
import { LangfuseClient } from "@langfuse/client";

// Reads ARIZE_API_KEY and ARIZE_BASE_URL (EU: https://api.eu-west-1a.arize.com)
const DATASET = process.env.ARIZE_DATASET_ID!; // dataset name or ID
const SPACE = process.env.ARIZE_SPACE_ID; // required when DATASET is a name
const DATASET_NAME = "support-golden";
const SKIP = new Set([
  "id",
  "createdAt",
  "updatedAt",
  "annotations",
  "input",
  "query",
  "question",
  "expected_output",
  "expectedOutput",
  "output",
  "answer",
]);

const examples: Record<string, unknown>[] = [];
let cursor: string | undefined;
while (true) {
  const { data, pagination } = await listDatasetExamples({
    dataset: DATASET,
    space: SPACE,
    limit: 500,
    cursor,
  });
  examples.push(...data);
  if (!pagination.hasMore) break;
  cursor = pagination.nextCursor;
  if (!cursor) break;
}

const langfuse = new LangfuseClient();
await langfuse.api.datasets.create({ name: DATASET_NAME });

for (const row of examples) {
  await langfuse.dataset.createItem({
    id: String(row.id), // stable retry key in Langfuse; this is the AX example id
    datasetName: DATASET_NAME,
    input: row.input ?? row.query ?? row.question ?? row,
    expectedOutput:
      row.expected_output ?? row.expectedOutput ?? row.output ?? row.answer,
    metadata: Object.fromEntries(
      Object.entries(row).filter(([key]) => !SKIP.has(key)),
    ),
  });
}

Step 3: Recreate judges and re-run experiments

Do not import AX experiment runs as Langfuse scores. AX's experiment UI stores outputs on the run. Scores from a homegrown judge (or extra JSON fields on POST /v2/experiments) often do not appear in the dataset Evaluations column. That column is tied to Evaluator Hub / published evals. Re-run a baseline in Langfuse so future experiments have an anchor.

from langfuse import get_client

langfuse = get_client()
dataset = langfuse.get_dataset("support-golden")


def task(*, item, **kwargs):
    return run_your_app(item.input)  # same path as production


dataset.run_experiment(name="after-migration", task=task)

Set up OpenTelemetry with LangfuseSpanProcessor first, or experiment traces will be thin. See tracing setup.

import { LangfuseClient } from "@langfuse/client";

const langfuse = new LangfuseClient();
const dataset = await langfuse.dataset.get("support-golden");

await dataset.runExperiment({
  name: "after-migration",
  task: async (item) => runYourApp(item.input), // same path as production
});
  • Evaluators: recreate LLM judges as managed or custom LLM-as-a-judge evaluators, or as code evaluators. If you previously called a model yourself to grade outputs, that grading call showed up in AX as its own LLM trace. In Langfuse, attach an evaluator to the experiment or to production traces instead of a second ad-hoc provider call.
  • Online evals: AX + Add Online Evaluator maps to Langfuse evaluators on live traffic, with their own sampling.

Step 4: Decide what to do with historical traces

Most teams cut over fresh: old traces stay in AX for the retention window, and Langfuse is the system of record from cutover day. Bulk-importing AX span exports or parquet is rarely worth it beyond a few showcase traces.

If you need a few historical traces

There is no turn-key history import. Re-run those requests, or send OpenTelemetry via the OTLP endpoint with x-langfuse-ingestion-version=4. Do not use the deprecated ingestion API.

Validation checklist

  • The AX exporter is gone from the app process; traces arrive as generations with token/cost
  • Agent traces still show CHAIN → LLM → TOOL (not LLM-only)
  • User and session attribution works if you set user.id / session.id
  • Dataset item count matches the AX export
  • An experiment run exists on the migrated dataset
  • Judges score that run (or a sample of production traces)
  • Prompts resolve by name+label if you migrated prompt hub
  • Team access set up (org/project roles, SSO if applicable)
  • AX exporter removed, or the parallel-run window has an end date

Limitations and gaps

  • AX ≠ Phoenix. Phoenix uses a self-hosted collector helper and phoenix.client. AX uses spaces, regional API hosts (api.{region}.arize.com), and a separate tracing exporter. Do not follow the Phoenix dataset snippet against AX.
  • Evals export is weak. Dataset Evaluations and experiment Evals stay empty unless scores were published through Evaluator Hub. REST extra fields on POST /v2/experiments (for example correctness_score, judge_label) do not fill those columns. A homegrown judge that calls the model itself shows up as a separate LLM trace, not as an eval on the experiment. Experiment-level cost / tokens / latency can also stay blank for standalone REST experiments even when the agent traces in the tracing project have those metrics. Treat experiment output as the portable artifact; recreate judges in Langfuse.
  • Online evals / alerts recreate as Langfuse evaluators and alerts.
  • AX dataset and experiment REST APIs are documented as beta.

FAQ

Do I have to re-instrument my application?

No. Keep OpenInference and switch the exporter. Keep manual CHAIN/TOOL spans. See Step 1 for the before/after in your SDK.

I am on Phoenix, not AX. Is this the right page?

No. Use Migrate from Arize Phoenix.

Can I evaluate old AX traces in Langfuse?

Evaluators run on data in Langfuse, so historical evaluation requires those traces to exist in Langfuse first. Start judges on new traffic at cutover.

Get help with the migration

Start on Langfuse Cloud or self-host. If you want a migration plan, talk to us.


Was this page helpful?

Last edited