Troubleshooting

Find your error message or code on this page. Every error the SDK throws has a code, a message, why it happened and how to fix it.

try {
  await bakery3.render();
} catch (error) {
  console.error(error.toDisplayString()); // message, why, fix and docs link
  error.code;       // never changes, so you can branch on it
  error.path;       // where in the scene, when the error has a place
  error.retryable;  // is sending the same request again worth trying?
}

An error you do not catch prints the same thing in the console, because its stack starts with the message, the why, the fix and the docs link.

Content hashing needs crypto.subtle

Content hashing needs crypto.subtle, which this page does not have.

Why: crypto.subtle is only exposed on secure origins (https, or localhost).
Fix: Serve your dev app over https or from localhost.
     Next.js: `next dev --experimental-https`.

Version 0.1.0 of the SDK shows this when you open your dev server on a LAN address, such as http://192.168.1.20:3000, because browsers hide crypto.subtle on a plain http address that is not localhost. Later versions hash without it, so update @oeave/bakery3. If you stay on 0.1.0, serve over https or use localhost.

Could not export the scene

Could not export the scene: <exporter message>

Why: The glTF exporter walks the scene graph; a geometry with a
     malformed attribute or a texture from a tainted canvas will
     stop it.
Fix: Run bakery3.check() to find the object it stopped on.

The common causes are:

A tainted canvas. A texture drawn from a cross-origin image without CORS cannot be read back. Serve the image with Access-Control-Allow-Origin, or set crossOrigin = 'anonymous' before loading it.

A malformed geometry attribute. This is a BufferAttribute with the wrong item size, or a NaN in a position buffer. check() names the object.

A texture that only exists on the GPU. A render target has no pixels the SDK can read. If a node material samples one, you get shader_texture_unreadable with the material named.

No scene to render

No scene for render().

Why: render() was called with no scene, and none has been attached.
Fix: In plain three.js, pass { scene, camera } to render(), or call
     bakery3.attach({ scene, camera, renderer }) once. In React Three
     Fiber, use useBakery3() inside or beside <Canvas>.

The message names the call that needed the scene: estimate(), bake() and inspect() say the same. In plain Three.js, call attach() once after you create the client:

const bakery3 = createBakery3({ token: getToken });
bakery3.attach({ scene, camera, renderer });

In R3F, the usual cause is <Bakery3Provider> mounted outside <Canvas>. Put it inside, because it reads the scene and camera from the canvas, or pass your key to the hook with useBakery3({ token }) and leave the provider out.

render() in Node

render() and inspect() read a live scene in a browser. In Node they throw CaptureFailed. To render from a server or a script, give renderBatch() the URLs of your .glb files.

Unsupported material

The material cannot be rendered faithfully. You have three options, best first. Change it to MeshStandardMaterial or MeshPhysicalMaterial, port it to TSL, or declare a material fallback.

check() names the material, its type and its path in the scene. See Materials.

Unsupported camera type

Pass one PerspectiveCamera or OrthographicCamera. Array, cube and stereo cameras are not supported. See Cameras.

The render is darker or flatter than my viewport

Attach your renderer. The SDK then reads outputColorSpace, toneMapping and toneMappingExposure from it.

bakery3.attach({ scene, camera, renderer });

In React Three Fiber, useBakery3() does this for you. See Environment & lighting.

The render is unlit, or much flatter than expected

⚠ environment_unavailable
The scene has an environment map, but its source pixels are not
reachable from the SDK.

Three.js has usually processed the HDRI on scene.environment, and the original image is gone. Tell the SDK where it came from:

rememberEnvironmentSource(hdri, '/studio.hdr');

@oeave/bakery3/r3f does this for drei's <Environment files>.

Could not upload

AssetUploadIncomplete: Could not upload scene.glb (148.2 MB): 403 Forbidden

Why: The presigned upload URL was rejected. These expire, and they are
     signed over an exact set of headers.
Fix: Retry the render. If it keeps failing, check for a service worker
     or proxy that rewrites request headers.

If it keeps failing, look for a service worker or a company proxy that adds, drops or rewrites a request header. That turns a valid upload into a 403. See Assets & caching.

Unauthorized, or 401 on every render

The message says which of these it is. Each one links to the Keys page, where you create and copy keys.

Message starts withWhat to do
No API key or token.Pass apiKey or token to createBakery3(). Sign up first if you have no account.
apiKey is empty.The environment variable that holds your key is not set. In a browser bundle only variables with your framework's public prefix (NEXT_PUBLIC_, VITE_) are set.
Replace the placeholderYou pasted the example bk_sk_.... Copy a real key.
token must be a functionPass a function that fetches a token, not the token itself.
That is not a Bakery3 key.The header carries something else, often a key for another service.
This API key or token is not valid.The key is mistyped or revoked. Copy a live one.

Other causes:

  • You passed a bk_pt_ browser token where a bk_sk_ secret key is needed. Making tokens, listing renders and deleting need the secret key.
  • You passed a browser token to new Bakery3Server(). Its first request refuses it and says so.
  • Your token route failed. When it answers with an HTML error page, the SDK says it did not return JSON. When it returns { error }, the SDK shows that error. Open the route in the network tab, and check your server log: often BAKERY3_SECRET_KEY is not set there.
  • Your token function returns the fetch Response and not the parsed body. Add .then((r) => r.json()).

OriginNotAllowed

The token was made with allowedOrigins, and the page's origin is not in the list. Include every origin you serve from, staging and preview deployments too. See Auth & keys.

TokenExpired

A browser token ran out and a new one could not take its place. That happens in two ways.

You passed a browser token as apiKey. Pass a function as token instead, so the SDK can ask for a new token when one expires:

// Stops working when the token expires:
createBakery3({ apiKey: 'bk_pt_…' });

// Fetches a fresh token when requested:
createBakery3({ token: () => mint() });

Or a render was started with a token that expired more than a day ago, and a new token cannot read it. Create tokens with a longer expiresInSeconds, or read the render from your server with your secret key.

RenderBudgetExceeded

The render could cost more than your maxCost, so it was refused before it started and cost nothing. Raise maxCost, lower the quality preset or reduce the output size. See Cost & estimates.

InsufficientBalance or QuotaExceeded

InsufficientBalance means the render's maximum price does not fit in your credit. We check the maximum and not the estimate, so a render can never overdraw. Top up on the Billing page. QuotaExceeded means a monthly limit you set yourself. See Limits.

RateLimited

Your key or token sent more calls in a minute than its limit. The SDK waits and tries again on its own, so you only see this once it has given up. Send fewer calls at once, or put a catalog in a batch. The limits are under Rate limits.

Cancelled or Expired

job.result() rejects with Cancelled when the render was cancelled, from your code or from the console. Expired means the render's files were deleted when the project's retention ended, or by hand. The record and its cost stay. To keep a result, copy its file into your own storage when it completes.

QueueFull

Renders over your concurrency limit wait in a queue. QueueFull means that queue is full, at 50 waiting renders per account. Wait for queued renders to start, cancel some, or move to a plan with more concurrency. For a whole catalog, use a batch. A batch is never refused as QueueFull.

BatchBudgetExceeded or BatchClosed

A page of batch items was refused whole, and nothing in it was started. BatchBudgetExceeded means the page would take the batch past its own maxCost. Raise it, or put the items in another batch. BatchClosed means the batch is sealed, paused, cancelled or completed. Resume a paused batch. Otherwise open a new batch with a new key, and the items that already completed are reused at no cost while they are still stored. See Render every variant.

QueueTimeout

A queued render waited 24 hours for a free slot and was dropped without a charge. You probably queued much more than your concurrency can work through. Send it again, or move to a plan with more concurrency.

OutOfVideoMemory

The scene has too many triangles, too many texture bytes, or both. Reduce whichever is larger, or drop a quality preset. See Scene size.

RenderTimeout

If the render timed out before it reached rendering, we already tried it once more for you. A timeout while rendering would happen again, so the render fails. Use fewer samples or a smaller output, or split the video. In practice this is an ultra render of a very large scene at 4K.

A render failed after every retry (attemptsExhausted)

WorkerLost: The render worker stopped reporting. (tried 3 times)

Fix: This was tried 3 times on different workers and failed the same way
     each time. We have been notified. Retry from the console, or
     POST /v1/renders/:id/retry, once we say it is fixed.

When a render fails on our side (WorkerFailure, WorkerLost, UploadFailed, ResultLost, RendererCrashed), we run it again on a different worker, up to the count under Retries. error.attemptsExhausted: true means every attempt failed the same way and we gave up. The render is failed, you paid nothing for any attempt, and we have been notified.

Once we have fixed the cause, press Retry in the console or call POST /v1/renders/:id/retry. You get a new render with the same request and metadata, checked against your balance and limits again. In a batch, POST /v1/batches/:id/retry does this for every such item at once.

When RendererCrashed happens twice on two machines, the scene is the likely cause. Try the advice under OutOfVideoMemory.

ProtocolVersionUnsupported

Your SDK's version does not match our workers, so the render was refused. Update @oeave/bakery3.

Unsupported animated property

The timeline refuses a property the renderer cannot animate. The error names the property, the object's path, the reason and what you can use instead. The properties you can animate are listed under Render video.

My animation stopped working after a scene rebuild

A hot reload or a variant swap replaces the objects your tracks point at. Point the tracks at the new objects:

const { healed, stale } = tl.reconcile(scene);
// healed: tracks that found their object again
// stale:  tracks that did not, each with a reason

Webhook signature does not match

Check that you pass the raw body, that you read the bakery3-signature header, that the secret belongs to this project, and that your server's clock is right to within five minutes. See Webhooks.

See what the SDK would send

inspect() returns the manifest a render would send. It renders nothing and costs nothing.

const manifest = await bakery3.inspect();

Read it next to the check() output to find a missing file, something the scene uses that is not supported, or a value that was captured wrong.