Arize Phoenix alternative: OTel tracing vs a gateway
Arize Phoenix is the OTel-native tracing-and-eval project a lot of teams reach for
first, so it deserves a real answer, not a table copied from two docs sites. We built
the same prompt — vip-support-triage, a support agent that changes tone for VIP
customers and lists their open tickets — on both platforms, then ran the identical
sequence on each: create the prompt, version it, send a live call, inspect the trace,
build a dataset, run an experiment, and call it from an SDK script.
Every paired screenshot below comes from the exact same prompt and the exact same
customer message, sent through the exact same downstream model
(openai/gpt-4o-mini via OpenRouter, configured as a Custom AI Provider in Phoenix) on
both platforms. Where a step is genuinely one-sided — no equivalent screen exists on
the other product — we say so instead of padding it out. License, pricing, team
structure, and community stats live on the compare page instead of
here — they were always tables, and a price change there is one edit instead of three.
At a glance
| Aspect | Phoenix | AcruxCore | Winner |
|---|---|---|---|
| Tracing depth | Single span, OTel semantic conventions, rich attribute tabs | Single automatic span | Phoenix |
| Prompt templating | Mustache — {{#section}} doubles as both if and for | Real nunjucks {% if %} / {% for %} logic | AcruxCore |
| Playground | Cost, tokens, latency shown inline | Cost, cache, latency shown inline | Tie |
| Request-path gateway | None — Playground calls proxy through a GraphQL mutation, SDK calls go direct | Built in — routing, caching, budgets | AcruxCore |
| Tool catalog | Ad-hoc per-prompt tool JSON, no catalog page | Versioned catalog, real executed calls, analytics | AcruxCore |
| Dataset creation | From a trace span, or manually | From real span-level trace feedback | Tie |
| SDK trace capture | Instrument a client + call the provider yourself | Automatic side effect of the gateway call | AcruxCore |
| Measured overhead | −14 to +21 ms across three 100-round runs | +11 to +51 ms across the same three runs | Phoenix, barely |
| Time-to-first-trace | SDK setup (register + instrument) before anything lands | Zero code, first gateway call | AcruxCore |
License, pricing, team structure, security, and community stats: see AcruxCore vs Phoenix on the compare page.
Full breakdown, screenshots, and the verdict below.
Tracing & observability
Same call, genuinely richer trace UI on Phoenix's side. Its Info/Attributes/Events tabs, per-span cost and token counts, and OTel semantic-convention attributes gave us more to look at than AcruxCore's single-span view — deserved credit even though this is the comparison's own template.
Show screenshots: trace detail on both platforms

| Feature | Phoenix | AcruxCore |
|---|---|---|
| Trace shape | Single span, rich attribute/event tabs | Single span per gateway call |
| How it's produced | Client-side OTel instrumentation (Playground calls proxy through Phoenix's own GraphQL backend) | Automatic — a side effect of the gateway call |
| Links back to prompt version | Not surfaced in this run | Yes — "View traces for this prompt version" |
Prompt authoring & versioning
Phoenix's Prompts feature only exists inside the Playground — there is no standalone
"create prompt" form, you build it live and save it from there. Its templating engine
is Mustache, selectable alongside F-String and no-templating. Mustache has no
separate if-statement: {{#is_vip}}...{{/is_vip}} is the same section syntax used
for looping over tickets, so a truthy scalar and an array are handled by one
construct, not two. That's a real difference from nunjucks, where {% if %} and
{% for %} are distinct.
Show screenshots: prompt editor, detail, and diff view

AcruxCore's own prompt editor — real nunjucks conditionals and loops rendering VIP status and the ticket list inline — is one sentence here rather than another screenshot; see the walkthrough for the picture.
Phoenix also has a real, separate diff view between versions — we didn't expect that
going in. But it exposed a structural gap: the Playground's input fields are flat text
boxes. Typing a JSON array into the tickets field for the Mustache loop to consume
doesn't parse it — Mustache treats the whole string as one truthy value, the loop runs
once with no bound fields, and the ticket lines render blank (- #: ) with no error.
The SDK renders it correctly (see SDK & DX below) because there the array is a real
Python list, not a string typed into a box — so this is a Playground-input limitation,
not a templating-engine one.
| Feature | Phoenix | AcruxCore |
|---|---|---|
| Conditional templating | Mustache sections — same syntax for if and for, verified hands-on | Real nunjucks {% if %}/{% for %}, rendered server-side |
| Version comparison | Real Diff toggle between any two versions | Standing Diff tab, unified diff between any two versions |
| Live/staging labels | production / staging tags on a version | production / staging aliases your app fetches at runtime |
| Structured variables in Playground | Flat text input only — arrays/objects don't parse, fail silently | Typed variable schema |
Sending a live call
Both need a real connected model before anything runs. Phoenix required one extra setup step: OpenAI-compatible providers with a custom base URL live under Custom AI Providers, separate from the built-in OpenAI/Anthropic/Bedrock/Gemini list, and we had to switch its API Type from "Responses" to "Chat Completions" for OpenRouter to accept the request.
Show screenshots: Playground run on both platforms

| Feature | Phoenix | AcruxCore |
|---|---|---|
| Playground | Runs against a saved prompt version, cost/tokens/latency shown inline | Runs against a stored prompt reference, shows cost/cache/latency inline |
| Custom OpenAI-compatible providers | Yes — separate "Custom AI Providers" section, needs the right API Type set | Native — any OpenAI-compatible base URL |
Where the platform sits — in the request path, or beside it
AcruxCore's gateway sits in the request path: every call is routed through it, so
BYOK provider selection, caching, budgets, and virtual keys all apply before the
provider is ever called. Phoenix sits beside the request path, on purpose — its
Playground's "Run" button doesn't call the provider from your browser at all; network
inspection during a real run showed the call going out as a single POST /graphql to
Phoenix's own backend, a UI-only relay with no routing, caching, or budget logic
behind it. Its SDK path (see SDK & DX below) skips that relay entirely and calls
the provider directly. That's a legitimate design choice — it works with any provider
without changing your call site — not a missing feature.
| Feature | Phoenix | AcruxCore |
|---|---|---|
| Where it sits | Beside the request path — Playground proxies via GraphQL, SDK calls go direct | In the request path — every call routes through it |
| BYOK, caching, budgets, virtual keys | Not applicable — no persistent request path to apply them to | Built in, applied before the provider call |
How tools are handled — schema registry, or execution
AcruxCore has a real Tool Catalog — callable functions, versioned like prompts, with their own analytics page. Phoenix has no equivalent nav item at all: the closest thing is a "+ Tool" control inside the Playground's message editor, which defines a JSON Schema function for that one prompt run. There's no catalog, no versioning, and nothing gets executed or measured — the schema is registered, never run.
Show screenshot: tool analytics on AcruxCore

AcruxCore's Tools page — a dozen versioned tools with real execution history — is one sentence here; see the walkthrough for the screenshot.
| Feature | Phoenix | AcruxCore |
|---|---|---|
| Tool definition | Ad-hoc JSON Schema per prompt, in the Playground | Versioned catalog entry, reusable across prompts |
| Execution & analytics | Not found | Real executed calls, volume/error-rate/P50-P95 per tool |
Evaluation & datasets
Phoenix's dataset flow starts from a real trace: select a span, "Add to Dataset,"
name it, done — genuinely as smooth as AcruxCore's. Its Evaluators page also draws a
clear diagram distinguishing LLM evaluators (AI-judged) from Code evaluators
(deterministic checks like exact_match and regex) — a distinction AcruxCore doesn't
surface as its own category, worth crediting.
Running an experiment against that dataset is where it broke down: the dataset stored
the span's rendered messages array, not the original template variables
(company, is_vip, tickets, question). Pointing the experiment at our saved
prompt produced "Dataset is missing input for variables" — the captured example and
the templated prompt don't speak the same shape without manual remapping.
Show screenshots: evaluators overview and the failed experiment run

AcruxCore's dataset (built from trace feedback) and its experiment run report are one sentence each here; see the walkthrough's dataset and run-history sections for the screenshots.
| Feature | Phoenix | AcruxCore |
|---|---|---|
| Dataset from a trace | Yes — select a span, add to dataset | Yes — from real span-level trace feedback |
| Evaluator types | LLM evaluators and deterministic Code evaluators, distinct paths | LLM-judged evaluation |
| Running a templated prompt against a trace-sourced dataset | Broke — dataset stores rendered messages, not template variables | Not attempted in this comparison |
SDK & DX
AcruxCore's SDK renders a stored prompt server-side
(hub.prompts.render()) and the gateway writes the trace with no tracing code in the
caller at all. Phoenix's Python SDK has no equivalent render call — there's no
server-side templating endpoint to hit, so the Mustache logic built in the Playground
had to be hand-duplicated in Python in our script. The upside: once instrumented,
OpenAIInstrumentor auto-wraps the OpenAI client and the trace lands correctly —
including the ticket loop rendering right, unlike the Playground's flat-input bug,
because here tickets is a real Python list.
from phoenix.otel import register
from openinference.instrumentation.openai import OpenAIInstrumentor
from openai import OpenAI
tracer_provider = register(
endpoint="http://localhost:6006/v1/traces",
project_name="vip-support-triage-sdk",
)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
client = OpenAI(api_key=OPENROUTER_KEY, base_url="https://openrouter.ai/api/v1")
# system message rendered by hand — Phoenix has no server-side prompt-render call
Full script:
px_trace_run.py.
AcruxCore's leg:
acx_sdk_run.py.
Show screenshot: SDK-produced trace on Phoenix

AcruxCore's SDK-produced trace, with prompt-version lineage attached automatically, is one sentence here; see the walkthrough for the screenshot.
| Feature | Phoenix | AcruxCore |
|---|---|---|
| Prompt rendering | No server-side render call — caller re-implements the template logic | hub.prompts.render() — one call, server-side |
| Tracing | Manual instrument step (register + OpenAIInstrumentor), then automatic | Automatic side effect of hub.gateway.chat(), zero tracing code |
Latency overhead — measured
100 interleaved rounds, three legs, rotating order, warm-up discarded — the same
method as every comparison in this series. phoenix_otel wraps a raw OpenAI client
with openinference-instrumentation-openai; acx_gateway is a raw POST to a local
AcruxCore gateway. Every leg ends at gpt-4o-mini on api.openai.com with the same
key and body, and the script refuses to start unless the gateway model resolves to a
native OpenAI credential — so all three provably share one upstream.
One run is not a result: the median gap moves enough between runs that a single number would mislead. So it ran three times.
| Run | Direct-call median | Phoenix gap | AcruxCore gap |
|---|---|---|---|
| 1 | 599 ms | +21 ms, CI [+2, +44] | +51 ms, CI [+27, +72] |
| 2 | 630 ms | −14 ms, CI [−30, +5] | +11 ms, CI [−3, +24] |
| 3 | 615 ms | +8 ms, CI [−11, +30] | +38 ms, CI [+16, +58] |
Phoenix's overhead lands between −14 ms and +21 ms, AcruxCore's between +11 ms and
+51 ms. Phoenix's was the lower of the two in all three runs, by roughly 30 ms —
consistent enough to be real, and small enough that no application would notice it.
The two are different kinds of cost: client-side OTel instrumentation pays CPU and
serialization, an extra network hop pays routing and caching. Full script:
latency_bench.py.
For a broader run — six platforms in one interleaved benchmark, the stored-prompt fetch timed alongside the completion, and four independent runs to check how stable the numbers are — see full-cycle latency across six LLM-ops platforms.
Friction hit during this run
Setting up the Custom AI Provider was straightforward once we found it under
Settings → AI Providers rather than the API Keys popover. Real friction we hit in this
run: the Playground's flat-text variable inputs silently mis-render structured data
(see Prompt authoring & versioning above); a dataset built from a trace can't drive
an experiment against a templated prompt without remapping (see Evaluation &
datasets above); and visiting /account on this no-auth local install crashes with
"Something went wrong" rather than a graceful empty state.
| Feature | Phoenix | AcruxCore |
|---|---|---|
| Time-to-first-trace | Requires register() + instrument() before any call | Zero code — first gateway call traces itself |
| Custom provider setup | Real feature, one extra field (API Type) to get right | Native BYOK, no extra step |
What Phoenix does that AcruxCore doesn't
Sourced from Phoenix's own dashboard and docs, not from this post's earlier aspects — those carry the same bias any of our own feature list would.
PXI, a built-in assistant docked in the dashboard. A chat panel lives in every Phoenix page, seeded with suggestions like "Find critical issues" and "Explain a concept," and it can answer questions about your own traces, not just about Phoenix in general. AcruxCore has no equivalent — no in-dashboard assistant at all.
Show screenshot: PXI, Phoenix's built-in assistant

Per-project data retention on a schedule, plus typed annotation configs. Project
Settings lets you set a retention policy with an actual schedule (this instance's
default runs "At 12:00 AM, only on Sunday (UTC)"), and define named annotation types
(here, a Categorical user_feedback config) rather than a single free-form rating.
AcruxCore's payload capture is one team-wide on/off switch; it has no per-project
retention schedule and no typed-annotation configuration.
Show screenshot: per-project retention and annotation config on Phoenix

One earlier expectation didn't hold up: Phoenix's docs list Tracing, Evaluation, Prompt Engineering, Datasets & Experiments, and PXI as its top-level capabilities today — no embedding-drift-analysis section turned up in the docs nav or in this instance's UI, so we're not carrying that claim forward from older material.
Is AcruxCore an Arize Phoenix alternative?
For running an LLM app, yes. For notebook-based analysis, no.
Phoenix has no request-path gateway, its "tool" is an unversioned JSON Schema saved per prompt that never executes, and the local open-source install has no team, role or audit concept at all — that last one is worth checking first if more than one person needs access.
Phoenix's per-span attribute depth is genuinely better than ours, and PXI, its docked assistant, has no equivalent here. If your work happens inside a notebook and OTel-native tracing is the whole job, Phoenix is the better fit.
We also accept OTel-instrumented traces on an OTLP endpoint, so keeping Phoenix's instrumentation while moving the calls themselves to our gateway is a supported combination rather than a fork in the road.
Verdict
| Phoenix | AcruxCore | |
|---|---|---|
| Strongest at | Rich per-span trace attributes, a genuine LLM-vs-code evaluator split, a built-in dashboard assistant | A request-path gateway with routing, caching, and budgets on every call; tools that actually execute and get measured; server-side prompt rendering with zero tracing code |
| Weakest at | No request-path gateway; no team/RBAC/audit at all in local OSS; Playground's flat inputs silently mis-render structured template data | No in-dashboard assistant; narrower eval-evaluator taxonomy; no audit export |
| Pick it if | You want OTel-native tracing and evals layered onto a client you already own, and don't need team/RBAC out of the box | You want the provider call itself — gateway, tools, feedback-driven datasets — traced for free, with a real team model, nothing bolted on after the fact; AcruxCore also accepts your own OTel-instrumented traces directly via its OTLP endpoint |
Phoenix's tracing depth and its LLM/code evaluator split (Tracing & observability, Evaluation & datasets above) are real strengths, and so is PXI, its built-in assistant (What Phoenix does that AcruxCore doesn't). But on the loop that actually runs a production LLM app day to day, this comparison landed on AcruxCore's side each time: Phoenix has no request-path gateway at all (Where the platform sits above), its "tool" is an unversioned JSON Schema saved per-prompt, never executed or measured (How tools are handled above), and its local OSS install has no team, role, or audit concept whatsoever. AcruxCore does all three natively, in one integrated flow. See the full picture, including license, pricing, and team structure, on the compare page.
Want to run this yourself? The Quickstart gets you from sign-up to a traced, gateway-routed call in about ten minutes.