Skip to main content
Voice agents fail in two different ways, and telling them apart is most of what error handling amounts to. A provider that returns a 503 will probably work on the next request. A provider that rejects your API key will reject it every time, and a bot that keeps retrying stays silent while it does. Pipecat separates the two with a single question: can this processor still do its job?

is_usable

Every FrameProcessor reports whether it can still work:
A processor stays usable through failures it might recover from, and becomes unusable once its work can no longer succeed — a rejected API key, an unknown model or voice, or enough consecutive failures that it has stopped trying. Being unusable changes what the framework does with it:
  • STT and TTS services stop accepting work, instead of failing once per chunk of audio or piece of text
  • WebSocket services stop reconnecting, instead of retrying credentials the provider has already rejected
  • A ServiceSwitcher fails over to another provider
The flag is set as the error is reported, so a handler reading frame.processor.is_usable sees the verdict that came with the error it’s handling.

ErrorCategory

Errors carry a category saying what kind of failure it was, independent of which provider produced it:
AUTHENTICATION, AUTHORIZATION, and INVALID_REQUEST are permanent: retrying gives the same result until credentials or settings change. Reporting one is what makes a processor unusable. Read category.is_permanent rather than listing the three yourself.
APPLICATION is how a service reports a failure in code it invoked — a tool handler, say. Those failures say nothing about the service’s own health, so they never make it unusable.

Deciding what the pipeline does

PipelineWorker takes a policy for what to do when a processor becomes unusable:
The policy is applied once per processor, not once per failed request — an unusable processor keeps failing for as long as the pipeline uses it, and the decision only needs making once. CONTINUE leaves the decision to your application, which is the right default when a ServiceSwitcher will fail over, or when the bot can carry on degraded. Choose END when a bot with a dead service has nothing useful left to do; the Pipecat examples use it, so an example stops rather than running on with a service that will never answer.

Reacting in your application

Handle on_error to decide for yourself:
on_usable_changed fires whenever a processor’s verdict changes, in either direction:

Recovering a service

An unusable processor stays that way until you say otherwise. Once whatever broke has been fixed — new credentials, a corrected voice ID — bring it back:
It then accepts work again, and a ServiceSwitcher will consider it a candidate. Switching to a service that is still marked unusable is refused, so recovery has to come first.

Reporting errors from your own processor

Passing a permanent category marks the processor unusable. When you know the processor is finished but the category doesn’t say so on its own — a retry budget exhausted, for instance — pass force_treat_as_permanent=True instead of inventing a category. Implement _classify_error() to map exceptions your processor understands onto a category. Errors carrying an HTTP status are classified from it automatically: 401 to AUTHENTICATION, 403 to AUTHORIZATION, 429 to RATE_LIMIT, 5xx to SERVER.

Migrating from fatal errors

ErrorFrame.fatal, the fatal argument of push_error(), and FatalErrorFrame are deprecated and will be removed in 2.0.0. A fatal error cancelled the pipeline outright, which conflated “this service is broken” with “this run is over” and left applications no say in the matter.
Passing fatal=True still cancels the pipeline, and warns.
Which replacement you want depends on what the error meant: The error leaves its processor unable to work. Report that, and let the policy decide:
ProcessorUnusablePolicy.CANCEL reproduces what fatal=True did. END and CONTINUE are usually better. The error isn’t about a processor’s state, but the pipeline should stop. Push the error, then end the pipeline explicitly:
Use EndWorkerFrame to drain queued frames, or CancelWorkerFrame to abandon them. To read errors, replace if frame.fatal: with a check on the processor:

Service Switching

Fail over to another provider automatically

PipelineWorker

Pipeline events, including on_pipeline_error

Frame Processor Events

on_error and on_usable_changed

System Frames

ErrorFrame and its fields