Skip to main content
Graduating from Phoenix to Arize AX breaks into two pieces of work, and you can do them in either order. This page covers the live path: getting new spans out of your running application and into AX. Moving the traces, evals, annotations, datasets, and experiments you already have in Phoenix is a separate one-time export and import, handled by the Phoenix to Arize AX migration tool. Most teams start sending traces to AX first and backfill afterward, since the live path is a handful of lines and the backfill needs a running Phoenix to read from.
Both products consume the same OpenInference telemetry, so your instrumentors, manual spans, and semantic conventions stay exactly as they are. The only thing that changes is how you register the tracer, meaning where spans get exported and how the request authenticates.

What Stays the Same

Begin Sending Traces to AX

1

Install the AX SDK

Keep arize-phoenix-otel installed while you work through this. You need it for the dual-write step, and uninstalling it is the last thing you do rather than the first.
2

Replace register() with the AX equivalent

Credentials are the one real difference between the two. Phoenix does not require any by default, and only takes an API key when you are on Phoenix Cloud or have turned on auth in a self-hosted deployment. AX always authenticates, using an API key together with a space ID. Both are on your project’s setup page in the AX UI.
Before, against a local Phoenix with no auth:
On Phoenix Cloud, or a self-hosted instance with auth enabled, you also pass api_key=... or set PHOENIX_API_KEY, which Phoenix turns into an authorization: Bearer header.After, against AX:
Everything below the register() call stays as it is.
3

Map the remaining arguments and environment variables

Most arguments have a direct counterpart. The two defaults that differ are called out in the rows below.Environment variables follow the same pattern.If your space is not in US East, set the endpoint to match. Use Endpoint.ARIZE_EUROPE for the EU, or the string https://otlp.ca-central-1a.arize.com/v1 for Canada. It is the same region as the subdomain you log in to.

Dual-Write While You Verify

A hard cutover is fine for a side project, but for production traffic you want a window where both backends see the same spans. Register AX once and attach a second exporter for Phoenix to that same tracer provider. Your instrumentation runs a single time, so the two backends receive identical spans and any difference you find is a configuration problem rather than a sampling artifact.
Register once, then attach a processor. If you call both phoenix.otel.register() and arize.otel.register() in the same process, whichever runs last owns the global tracer provider and the other destination goes quiet without raising an error.
Leave this running long enough to cover a representative slice of traffic, including your slowest and most deeply nested requests. Then compare the two projects on four things:
  1. Trace counts should match over the same time window.
  2. Span trees should have the same shape. Pick a few traces by hand and check nesting depth and span kinds.
  3. Attributes should be equally complete, particularly inputs, outputs, token counts, and any custom metadata or tags you set.
  4. Sessions and users should still group correctly if you rely on session.id, since that grouping is the easiest thing to lose in a rushed cutover.
When the two agree, set DUAL_WRITE_PHOENIX=0 and deploy. Keeping the toggle in an environment variable means a rollback is a config change instead of a code change and a rebuild.

Retire the Phoenix Path

Once AX has been the only destination through a full traffic cycle, remove arize-phoenix-otel from your dependencies, drop the PHOENIX_* variables from your deployment config, and delete the dual-write block. If you still intend to backfill, do that before you decommission Phoenix, because the migration tool reads from a running instance. Evals and annotations need their own pass. They are separate write paths from tracing, so code that logged them to Phoenix keeps doing exactly that after you change the tracer. See Run evals on traces and Human review for the AX equivalents.

Troubleshooting

Check credentials first, since they are the usual cause. Header naming depends on transport: the HTTP endpoint expects the hyphenated arize-space-id and arize-api-key, while gRPC expects the unprefixed space_id and api_key as metadata. Using the wrong form fails silently and you get no spans at all. See Manual instrumentation for the full breakdown. If you are using register(), leave verbose=True on and read the configuration it prints at startup.
The project name never made it onto the resource attribute. In Python, pass project_name to register() or set ARIZE_PROJECT_NAME. In JS/TS you have to set SEMRESATTRS_PROJECT_NAME on the resource yourself, since there is no argument for it to inherit.
Confirm that you registered once and attached a processor, rather than calling both products’ register() functions. If that looks right, check that you are comparing the same time window in both UIs and give AX a minute to index recent spans.
arize.otel.register() has no auto_instrument argument in the SDK versions most applications run, so attach each instrumentor explicitly with .instrument(tracer_provider=tracer_provider). The extra lines are worth it, because being explicit forces you to instrument the framework or model client that actually makes the LLM calls rather than only the web layer around it.
AX batches spans by default, while Phoenix uses a simple processor that exports synchronously, so a script that exits immediately can lose its last spans. Pass batch=False, or call tracer_provider.shutdown() before the process exits.

Next step

Backfill your Phoenix history

Export your traces, evals, annotations, datasets, and experiments out of Phoenix and import them into AX.

Configure your tracer

The full parameter reference, including transports, regional endpoints, and raw OTel setup.