Skip to main content
Issue: Streaming logs fail with initialization or forbidden errors when apiUrl, appUrl, or BRAINTRUST_API_URL point to conflicting environments.Cause: The SDK sends logging requests to the API/data plane URL. If apiUrl is set, it takes precedence over appUrl.Fix: For Braintrust-hosted projects, remove custom URL settings unless you use a custom frontend. For self-hosted or hybrid projects, set only apiUrl or BRAINTRUST_API_URL to the data plane endpoint.
import { initLogger } from "braintrust";

initLogger({
  projectName: process.env.BRAINTRUST_PROJECT_NAME,
  apiKey: process.env.BRAINTRUST_API_KEY,
  apiUrl: "https://your-data-plane.example.com",
});
Issue: Evals fail, time out, produce incomplete results, or hit EMFILE: too many open files.Cause: Eval() runs tasks with unlimited concurrency unless maxConcurrency is set. That can exhaust file descriptors, memory, database pools, or model-provider rate limits.Fix: Set maxConcurrency to a small value such as 10, then tune up or down based on the workload.
import { Eval } from "braintrust";

await Eval("my-project", {
  data: myDataset,
  task: myTask,
  scores: [myScorer],
  maxConcurrency: 10,
});
Issue: braintrust eval prints results successfully, but the Node.js process does not exit.Cause: Open handles in your eval process can keep the Node.js event loop alive. Common sources include database pools, Redis clients, WebSockets, and HTTP keep-alive agents.Fix: Run the eval with the Node inspector to identify active handles, then explicitly close external resources after the eval finishes.
node --inspect-brk ./node_modules/.bin/braintrust eval your-eval.ts
process.on("beforeExit", async () => {
  await mongoClient.close();
  await redis.disconnect();
});
Issue: Traces don’t appear, show empty fields, never leave “in progress”, or don’t include final values.Cause: A process can exit before logs flush, or an exception can bypass end() on a manually-created span.Fix: Prefer traced() — it ends the span for you, even when the wrapped function throws. If you use startSpan() directly, call end() in finally. Either way, call flush() before a short-lived process exits.
import { flush, startSpan, traced } from "braintrust";

await traced(async (span) => {
  span.log({ input, output });
});

const span = startSpan({ name: "manual-operation" });
try {
  span.log({ input });
} finally {
  span.end();
  await flush();
}
Issue: Follow-up turns in an existing conversation stop appearing under the original trace after a server restart or pod replacement.Cause: Exported span context from span.export() was kept only in memory. After restart, the SDK cannot continue the previous trace.Fix: Persist the exported span context. On the next request, wrap the work in withParent(), or pass the stored value as parent to traced() or startSpan().
import { traced, withParent } from "braintrust";

const parent = await traced((span) => span.export(), { name: "conversation" });
await db.set(conversationId, parent);

const storedParent = await db.get(conversationId);
if (storedParent) {
  await withParent(storedParent, async () => {
    await traced(async (span) => {
      span.log({ input: nextMessage });
    });
  });
}
Issue: prompt.version returns a large decimal string, while the Braintrust UI shows a short hexadecimal version ID.Cause: The SDK returns the raw transaction ID. The UI displays a prettified, reversible version of the same ID.Fix: Convert SDK values to the UI form with prettifyXact(), or back with loadPrettyXact(). Both are exported from braintrust/util.
import { loadPrettyXact, prettifyXact } from "braintrust/util";

const uiVersion = prettifyXact(prompt.version);
const rawVersion = loadPrettyXact(uiVersion);
Issue: The experiment view shows Prompt: None, even though your task uses a prompt.Cause: On LLM spans, the UI detects prompts from prompt metadata. Hard-coded prompts or prompts sent without the Braintrust prompt workflow do not attach that metadata.Fix: Fetch the prompt with loadPrompt(), call prompt.build({ input }) to produce the messages and parameters, then send them through a wrapped client (wrapOpenAI, wrapAnthropic, etc.). The wrapper reads the metadata from build() and attaches it to the LLM span.
import { loadPrompt, wrapOpenAI } from "braintrust";
import OpenAI from "openai";

const client = wrapOpenAI(new OpenAI());
const prompt = await loadPrompt({
  projectName: "my-project",
  slug: "summarizer",
});

const { messages, ...parameters } = prompt.build({ input });

await client.responses.create({
  ...parameters,
  input: messages,
});
Issue: A prompt configured for a GPT-5 model returns temperature from prompt.build(), and passing those parameters to OpenAI fails.Cause: Braintrust prompts can store temperature regardless of the selected model, but GPT-5 models reject temperature on the OpenAI API.Fix: Strip temperature from the parameters before sending the request, as shown below.
const { temperature: _temperature, messages, ...parameters } = prompt.build(variables);

await client.responses.create({
  ...parameters,
  input: messages,
});
Issue: You see only some traces, or none at all, from a deployment on Vercel.Cause: Vercel freezes the function as soon as it returns a response, so events still in Braintrust’s buffer may never flush.Fix: Call flush() in your application code after your AI calls. On Vercel, flush() sends buffered data even after the response has been returned.
await client.responses.create();

// Flushes buffered data, even on Vercel. flush() returns a Promise but does not need to be awaited.
flush();
Issue: Enabling auto-instrumentation crashes the Node.js process with a fatal diagnostics_channel assertion.
# /usr/local/bin/node[1]: uint32_t node::diagnostics_channel::BindingData::GetOrCreateChannelIndex(const std::string&) at ../src/node_diagnostics_channel.cc:53
# Assertion failed: (next_channel_index_) < (kMaxChannels)
----- Native stack trace -----
----- JavaScript stack trace -----
1: Channel (node:diagnostics_channel:196:32)
2: channel (node:diagnostics_channel:247:10)
3: tracingChannelFrom (node:diagnostics_channel:281:12)
4: TracingChannel (node:diagnostics_channel:301:16)
5: tracingChannel (node:diagnostics_channel:446:10)
6: isomorph_default.newTracingChannel (file:///usr/src/app/node_modules/braintrust/dist/chunk-XXXXXXXX.mjs:00000:00)
7: tracingChannel (file:///usr/src/app/node_modules/braintrust/dist/chunk-XXXXXXXX.mjs:000:00)
8: traceSyncStreamChannel (file:///usr/src/app/node_modules/braintrust/dist/chunk-XXXXXXXX.mjs:00000:00)
Cause: Node’s built-in diagnostics_channel module keeps a fixed-size table of tracing channels, capped at 1024 as of Node.js v26.5.0. Each auto-instrumentation registers channels in that table, so kMaxChannels is reached once too many instrumentations load. This is most common when Braintrust runs alongside another APM instrumentation such as Datadog, New Relic, or Sentry, which also register channels.Fix: Reduce the number of active instrumentations so the table stays under the limit. Disable unused Braintrust integrations with BRAINTRUST_DISABLE_INSTRUMENTATION, a comma-separated list of integrations to skip, and disable unused instrumentations in your other APM agent.
BRAINTRUST_DISABLE_INSTRUMENTATION=openai,anthropic node --import braintrust/hook.mjs app.js