> ## Documentation Index
> Fetch the complete documentation index at: https://arize-ax.mintlify.site/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Graduate from Phoenix to Arize AX

> Begin sending traces from a running application to Arize AX instead of Phoenix, dual-write to both while you verify parity, then finish the cutover.

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](https://arize.com/docs/phoenix/resources/phoenix-to-arize-ax-migration).

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.

<Note>
  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.
</Note>

## What Stays the Same

| Unchanged                   | Details                                                                                                                                                                                                                                                     |
| :-------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Instrumentor packages       | The `openinference-instrumentation-*` packages are shared by both products, so you keep the same versions and the same `.instrument()` calls.                                                                                                               |
| Manual spans and decorators | Span kinds, attribute names, and the OpenInference semantic conventions are identical on both sides.                                                                                                                                                        |
| Project names               | Both products group spans by the `openinference.project.name` resource attribute, so your existing project names carry over as they are.                                                                                                                    |
| Context managers            | `using_session`, `using_user`, `using_metadata`, `using_tags`, `using_prompt_template`, and `suppress_tracing` all still work. Import them from `openinference.instrumentation`, which is where they come from anyway. `phoenix.otel` only re-exports them. |

## Begin Sending Traces to AX

<Steps>
  <Step title="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.

    <CodeGroup>
      ```bash Python theme={null}
      pip install "arize[otel]"
      ```

      ```bash JS/TS theme={null}
      npm install @arizeai/openinference-semantic-conventions @opentelemetry/exporter-trace-otlp-proto
      ```
    </CodeGroup>
  </Step>

  <Step title="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.

    <Tabs>
      <Tab title="Python">
        Before, against a local Phoenix with no auth:

        ```python theme={null}
        from phoenix.otel import register
        from openinference.instrumentation.openai import OpenAIInstrumentor

        tracer_provider = register(
            project_name="my-llm-app",
            endpoint="http://localhost:6006/v1/traces",
        )
        OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
        ```

        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:

        ```python theme={null}
        import os

        from arize.otel import register
        from openinference.instrumentation.openai import OpenAIInstrumentor

        tracer_provider = register(
            space_id=os.environ["ARIZE_SPACE_ID"],
            api_key=os.environ["ARIZE_API_KEY"],
            project_name="my-llm-app",
        )
        OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
        ```

        Everything below the `register()` call stays as it is.
      </Tab>

      <Tab title="JS/TS">
        Neither product ships a single `register()` helper for JS/TS, so the change is limited to the exporter and the project resource attribute.

        | Setting        | Phoenix                                                                         | Arize AX                                                                                                      |
        | :------------- | :------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------ |
        | Exporter `url` | `http://localhost:6006/v1/traces`, or your Phoenix Cloud URL                    | `https://otlp.arize.com/v1/traces`                                                                            |
        | Auth headers   | None on a local instance, or `authorization: Bearer <key>` when auth is enabled | `arize-space-id` and `arize-api-key`, always required                                                         |
        | Project        | The `openinference.project.name` resource attribute                             | The same attribute, set through `SEMRESATTRS_PROJECT_NAME` from `@arizeai/openinference-semantic-conventions` |

        ```typescript theme={null}
        new BatchSpanProcessor(
          new OTLPTraceExporter({
            url: "https://otlp.arize.com/v1/traces",
            headers: {
              "arize-space-id": process.env.ARIZE_SPACE_ID!,
              "arize-api-key": process.env.ARIZE_API_KEY!,
            },
          })
        )
        ```

        For a complete `NodeTracerProvider` example, see [Configure your tracer](/docs/ax/instrument/configure-your-tracer).
      </Tab>
    </Tabs>
  </Step>

  <Step title="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.

    | `phoenix.otel.register`                 | `arize.otel.register`                          | Details                                                                                                                                            |
    | :-------------------------------------- | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `api_key`, optional                     | `api_key` and `space_id`, both required        | Phoenix only needs a key when auth is enabled. AX always authenticates and scopes the key to a space, and Phoenix has no equivalent of `space_id`. |
    | `project_name`                          | `project_name`                                 | Same meaning, and both default to `"default"`.                                                                                                     |
    | `endpoint`                              | `endpoint`                                     | AX defaults to `Endpoint.ARIZE`, which is `https://otlp.arize.com/v1`, rather than to a local address.                                             |
    | `protocol="grpc"` or `"http/protobuf"`  | `transport=Transport.GRPC` or `Transport.HTTP` | AX takes an enum from `arize.otel` instead of a string literal.                                                                                    |
    | `batch=False` by default                | `batch=True` by default                        | AX batches spans unless you say otherwise, which is what you want in production. Pass `batch=False` for tests and short-lived scripts.             |
    | `headers`                               | `headers`                                      | Extra headers, merged with the credential headers.                                                                                                 |
    | `set_global_tracer_provider`, `verbose` | Same names, same defaults                      | Nothing to change.                                                                                                                                 |
    | `auto_instrument=True`                  | No equivalent                                  | Attach each instrumentor explicitly. There is a troubleshooting note on this below.                                                                |

    Environment variables follow the same pattern.

    | Phoenix                                      | Arize AX                                                 |
    | :------------------------------------------- | :------------------------------------------------------- |
    | `PHOENIX_COLLECTOR_ENDPOINT`                 | `ARIZE_COLLECTOR_ENDPOINT`                               |
    | `PHOENIX_API_KEY`, only when auth is enabled | `ARIZE_API_KEY` and `ARIZE_SPACE_ID`, both always needed |
    | `PHOENIX_PROJECT_NAME`                       | `ARIZE_PROJECT_NAME`                                     |
    | `PHOENIX_CLIENT_HEADERS`                     | Pass `headers=` to `register()`                          |

    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.
  </Step>
</Steps>

## 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.

<Warning>
  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.
</Warning>

```python theme={null}
import os

from arize.otel import register
from phoenix.otel import BatchSpanProcessor as PhoenixSpanProcessor
from openinference.instrumentation.openai import OpenAIInstrumentor

tracer_provider = register(
    space_id=os.environ["ARIZE_SPACE_ID"],
    api_key=os.environ["ARIZE_API_KEY"],
    project_name="my-llm-app",
)

if os.getenv("DUAL_WRITE_PHOENIX") == "1":
    # Only send an auth header if your Phoenix requires one. A local
    # instance without auth needs no headers at all.
    phoenix_key = os.getenv("PHOENIX_API_KEY")
    tracer_provider.add_span_processor(
        PhoenixSpanProcessor(
            endpoint=os.environ["PHOENIX_COLLECTOR_ENDPOINT"],
            headers={"authorization": f"Bearer {phoenix_key}"} if phoenix_key else None,
        )
    )

OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
```

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](https://arize.com/docs/phoenix/resources/phoenix-to-arize-ax-migration) 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](/docs/ax/evaluate/run-evals-on-traces) and [Human review](/docs/ax/evaluate/human-review) for the AX equivalents.

## Troubleshooting

<AccordionGroup>
  <Accordion title="No spans arrive in AX">
    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](/docs/ax/instrument/manual-instrumentation) for the full breakdown. If you are using `register()`, leave `verbose=True` on and read the configuration it prints at startup.
  </Accordion>

  <Accordion title="Spans land in a project called default">
    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.
  </Accordion>

  <Accordion title="Only one of the two backends is receiving spans">
    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.
  </Accordion>

  <Accordion title="auto_instrument=True no longer works">
    `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.
  </Accordion>

  <Accordion title="Short scripts exit before spans flush">
    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.
  </Accordion>
</AccordionGroup>

***

## Next step

<CardGroup cols={2}>
  <Card title="Backfill your Phoenix history" icon="clock-rotate-left" href="https://arize.com/docs/phoenix/resources/phoenix-to-arize-ax-migration">
    Export your traces, evals, annotations, datasets, and experiments out of Phoenix and import them into AX.
  </Card>

  <Card title="Configure your tracer" icon="sliders" href="/docs/ax/instrument/configure-your-tracer">
    The full parameter reference, including transports, regional endpoints, and raw OTel setup.
  </Card>
</CardGroup>
