You shipped the auto-publishing agent in the evening, and an hour later it starts returning "this model is currently unavailable". Retries change nothing. An hour later it recovers, and then the same thing happens again at midnight. I have lived this loop on six auto-publishing pipelines I run as an indie developer at Dolice, especially during the weekend right after a new Claude model lands.
This one is not a 529 Overloaded and not a 429 rate limit. It is a per-model availability error tied to the exact model id you sent. Retrying with the same id will not fix it — you have to switch the id. So the right design is not "back off and wait" but "step down to a different model on the same request".
Here is what I learned about when it fires, and the fallback pattern that finally stopped my pipelines from going dark.
Five conditions where the error actually fires
Going back through six months of logs and support tickets, almost everything falls into five buckets:
- First few hours after a new model release: when Claude Sonnet 4.6 rolled out, an older SDK using the new id was unstable until the region-by-region rollout completed
- Deprecation window for an older model id: ids with an announced deprecation date can return availability errors intermittently in the days leading up to the cutoff
- Region-specific missing variants: across AWS Bedrock or Vertex AI a model may be available in
us-west-2but notap-northeast-1. Less common on Anthropic direct API, but real on multi-cloud setups - Invalid combination of beta feature flags: enabling extended thinking + parallel tool_use + computer use at the same time can fall outside the active rollout for some hours
- Account-tier permission violation: a Tier 1 account requesting a Tier 2 model — the rejection comes back through this same message
My own incident split was roughly 40% / 15% / 5% / 25% / 15%. Cases 1 and 4 together cover two thirds of the incidents, so they are where the engineering pays off.
A static fallback table per task tier
The single most effective fix is having a declared fallback chain for each task. I keep them keyed by tier.
type ModelTier = "frontier" | "balanced" | "fast";
interface ModelFallbackChain {
primary: string;
fallbacks: string[];
}
const FALLBACK_CHAINS: Record<ModelTier, ModelFallbackChain> = {
frontier: {
primary: "claude-opus-4-6",
fallbacks: ["claude-sonnet-4-6", "claude-sonnet-4-5"],
},
balanced: {
primary: "claude-sonnet-4-6",
fallbacks: ["claude-sonnet-4-5", "claude-haiku-4-5-20251001"],
},
fast: {
primary: "claude-haiku-4-5-20251001",
fallbacks: ["claude-haiku-4-5", "claude-haiku-4"],
},
};Each task says "I want frontier — but rather than stop, drop down to balanced". After this change my pipeline's average downtime during a Claude Sonnet 4.6 weekend release window went from roughly 3 hours to under 8 minutes. That alone is the most impactful resilience change I made this year, and the implementation is under 50 lines.
Distinguishing the error and stepping through the chain
The HTTP status sits in the 400 family, so you can't fall back on status code alone. Inspect the SDK's structured error body.
import Anthropic from "@anthropic-ai/sdk";
async function invokeWithFallback(
client: Anthropic,
tier: ModelTier,
buildRequest: (modelId: string) => Anthropic.Messages.MessageCreateParams
): Promise<Anthropic.Messages.Message> {
const chain = FALLBACK_CHAINS[tier];
const candidates = [chain.primary, ...chain.fallbacks];
let lastError: unknown;
for (const modelId of candidates) {
try {
const req = buildRequest(modelId);
return await client.messages.create(req);
} catch (err) {
if (isModelUnavailable(err)) {
console.warn(`model ${modelId} unavailable, trying next`);
lastError = err;
continue;
}
// Any other error: do not silently fall back. Throw upward.
throw err;
}
}
throw lastError;
}
function isModelUnavailable(err: unknown): boolean {
if (!err || typeof err !== "object") return false;
const e = err as { status?: number; error?: { type?: string; message?: string } };
if (e.status !== 400 && e.status !== 404) return false;
const msg = e.error?.message?.toLowerCase() ?? "";
return (
msg.includes("currently unavailable") ||
msg.includes("model is not available") ||
msg.includes("model_not_available") ||
e.error?.type === "model_not_found_error"
);
}The discipline here is to scope the fallback to availability errors only. If you also catch rate limits or 529s and step down to a smaller model, you just propagate the upstream pressure to the smaller pool. Keep the predicate narrow.
Capability matrix so a fallback target doesn't reject features
Cause 4 (invalid feature-flag combination) is what trips fallback chains in production. My Lacrima and Mystery blog pipelines run extended thinking together with parallel tool_use; on Sonnet 4.6 it works, but on the 4.5 fallback the same combination intermittently returns availability errors. The fix is keeping a separate capability map for each model and stripping features when stepping down.
interface ModelCapabilities {
modelId: string;
extendedThinking: boolean;
parallelToolUse: boolean;
computerUse: boolean;
}
const CAPABILITY_MATRIX: Record<string, ModelCapabilities> = {
"claude-opus-4-6": {
modelId: "claude-opus-4-6",
extendedThinking: true,
parallelToolUse: true,
computerUse: true,
},
"claude-sonnet-4-6": {
modelId: "claude-sonnet-4-6",
extendedThinking: true,
parallelToolUse: true,
computerUse: false,
},
"claude-sonnet-4-5": {
modelId: "claude-sonnet-4-5",
extendedThinking: true,
parallelToolUse: true,
computerUse: false,
},
"claude-haiku-4-5-20251001": {
modelId: "claude-haiku-4-5-20251001",
extendedThinking: false,
parallelToolUse: true,
computerUse: false,
},
};
function buildRequestForModel(
modelId: string,
basePrompt: string,
tools: Anthropic.Messages.Tool[]
): Anthropic.Messages.MessageCreateParams {
const caps = CAPABILITY_MATRIX[modelId];
return {
model: modelId,
max_tokens: 4096,
messages: [{ role: "user", content: basePrompt }],
tools,
...(caps.extendedThinking ? { thinking: { type: "enabled", budget_tokens: 16384 } } : {}),
};
}The whole point is: "I'd rather drop extended thinking than fail completely". The last-ditch model in the chain has the fewest features enabled, optimized for "still responds, even if a bit less smart".
Account-tier rejections need a separate alert path
Cause 5 doesn't recover via fallback. A Tier 1 account asking for a Tier 2 model has to either upgrade or use a different key. If you let it flow through the same fallback loop, your production logs fill with noise. I split this out as its own predicate and route it to a Slack alert.
function isAccountTierViolation(err: unknown): boolean {
if (!err || typeof err !== "object") return false;
const e = err as { error?: { message?: string } };
const msg = e.error?.message?.toLowerCase() ?? "";
return (
msg.includes("not authorized") ||
msg.includes("tier") ||
msg.includes("upgrade your account")
);
}Tier violations get their own retry-zero, alert-immediately path in my pipeline. They never enter the fallback loop.
Observing how often the chain actually steps down
The most overlooked part of a fallback design is observability. I log every invocation with the depth at which it succeeded.
interface ModelInvocationLog {
timestamp: string;
intendedModel: string;
actualModel: string;
fallbackDepth: number; // 0 = primary, 1 = first fallback, ...
taskType: string;
outcome: "success" | "all_failed";
}I aggregate this weekly. If more than 5% of invocations land on fallbackDepth >= 1, that is the signal to revisit the configuration. The pattern I see is a 10–15% spike for the first one or two weeks after a new model release, settling back to roughly 2% afterward.
Reproducing it locally for testing
A small dev tip: this error only fires in production, so testing the fallback path is awkward. Sending a deliberately non-existent model id (for example claude-opus-99-xxx) returns a nearly identical response shape and is plenty good enough for a unit test that exercises the entire fallback chain. I run that test on every CI build now — ever since the one production incident where my fallback code "looked right" but I had never actually exercised the second step.
If you see this error, suspect your model id selection and your beta-feature combination first. If the issue lingers, invest in a real fallback design. After six months of running this in production, my off-hours incidents around new model rollouts went almost entirely silent — and that alone has been worth the effort.