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();