Introduction to Error Recovery in llama.cpp
llama.cpp is a popular C/C++ inference engine for running large language models locally. While it is fast and lightweight, real-world deployments face a variety of failure modes: malformed input, context overflow, OOM conditions, model loading failures, and network timeouts when streaming from remote backends. Building robust applications requires explicit error recovery patterns that detect failures, degrade gracefully, and resume operation where possible.
This tutorial walks through the most important error recovery patterns for llama.cpp-based applications, with practical C++ examples you can adapt to your own projects.
Why Error Recovery Matters
Unlike managed cloud APIs, llama.cpp runs on heterogeneous hardware with unpredictable memory constraints. A prompt that works on a 32 GB workstation may crash on a laptop with 8 GB of RAM. Additionally, llama.cpp exposes low-level primitives — tokenizers, context objects, sampling state — that require careful lifecycle management. Without recovery patterns, a single failure can leave the process in an inconsistent state, leak memory, or produce corrupted output.
- Stability: Long-running services must survive transient failures without restarting.
- Resource safety: Failed inference calls can leak context memory if not cleaned up.
- User experience: Graceful degradation (truncation, retry, fallback) is preferable to hard crashes.
- Debuggability: Structured error handling makes root-cause analysis easier.
Core Failure Modes in llama.cpp
Before writing recovery code, it helps to categorize the failures you will encounter:
- Model load failures: Missing file, corrupt weights, unsupported format, insufficient memory.
- Context allocation failures:
llama_new_context_with_modelreturns null when GPU/CPU memory is exhausted. - Tokenization failures: Empty input, invalid UTF-8, or input exceeding the model's vocabulary constraints.
- Context overflow: Prompt + generated tokens exceed
n_ctx, causing degraded or undefined behavior. - Decoding failures:
llama_decodereturns non-zero when batch dimensions mismatch or KV cache is full. - Sampling failures: Empty logits, invalid grammar constraints, or sampler state corruption.
Pattern 1: Guarded Resource Initialization
The first line of defense is checking every allocation. llama.cpp uses C-style APIs that return null pointers or error codes rather than throwing exceptions. Wrap these calls in guard functions.
#include "llama.h"
#include <stdexcept>
#include <string>
struct llama_model_ptr {
llama_model* ptr;
explicit llama_model_ptr(llama_model* p) : ptr(p) {}
~llama_model_ptr() { if (ptr) llama_model_free(ptr); }
llama_model_ptr(const llama_model_ptr&) = delete;
llama_model_ptr& operator=(const llama_model_ptr&) = delete;
};
llama_model_ptr load_model_or_throw(const std::string& path) {
llama_model_params params = llama_model_default_params();
params.n_gpu_layers = 99; // attempt full offload
llama_model* model = llama_model_load_from_file(path.c_str(), params);
if (!model) {
throw std::runtime_error("Failed to load model: " + path);
}
return llama_model_ptr(model);
}
This RAII wrapper ensures the model is freed even if an exception propagates upward. The same pattern applies to llama_context.
Pattern 2: Context Allocation Fallback
When GPU memory is insufficient, llama_new_context_with_model may fail. A robust loader retries with progressively smaller configurations.
llama_context* create_context_with_fallback(llama_model* model, int target_ctx) {
struct Attempt {
int n_ctx;
int n_gpu_layers;
int n_batch;
};
std::vector<Attempt> attempts = {
{target_ctx, 99, 512},
{target_ctx, 0, 512}, // CPU fallback
{target_ctx / 2, 0, 256}, // shrink context
{1024, 0, 128}, // minimal viable
};
for (const auto& a : attempts) {
llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = a.n_ctx;
ctx_params.n_batch = a.n_batch;
ctx_params.n_gpu_layers = a.n_gpu_layers;
llama_context* ctx = llama_new_context_with_model(model, ctx_params);
if (ctx) {
fprintf(stderr, "Context created: n_ctx=%d, gpu_layers=%d\n",
a.n_ctx, a.n_gpu_layers);
return ctx;
}
fprintf(stderr, "Context attempt failed: n_ctx=%d, gpu_layers=%d\n",
a.n_ctx, a.n_gpu_layers);
}
return nullptr;
}
This pattern lets your application start in a degraded mode rather than refusing to run at all.
Pattern 3: Context Overflow Recovery
One of the most common runtime errors is exceeding n_ctx. The recovery strategy is to truncate the prompt or implement a sliding window. The safest approach is to check token counts before decoding.
bool safe_tokenize(llama_model* model,
const std::string& text,
std::vector<llama_token>& out_tokens,
int max_tokens,
bool add_bos) {
out_tokens.resize(max_tokens);
int n = llama_tokenize(model, text.c_str(), text.size(),
out_tokens.data(), max_tokens, add_bos, true);
if (n < 0) {
// Buffer too small — retry with exact required size
out_tokens.resize(-n);
n = llama_tokenize(model, text.c_str(), text.size(),
out_tokens.data(), -n, add_bos, true);
}
if (n < 0) return false;
out_tokens.resize(n);
int n_ctx = llama_n_ctx_from_model(model);
if ((int)out_tokens.size() > n_ctx - 4) {
// Reserve space for generation; truncate from the front
int keep = n_ctx - 4;
out_tokens.erase(out_tokens.begin(),
out_tokens.begin() + (out_tokens.size() - keep));
fprintf(stderr, "Warning: prompt truncated to %d tokens\n", keep);
}
return true;
}
Truncating from the front preserves the most recent context, which is usually more relevant for conversational applications. For document QA, you may prefer to truncate from the back or split into chunks.
Pattern 4: Decode Error Handling
llama_decode returns a status code. A return value of 1 indicates the KV cache is full; 2 indicates a dimension mismatch. Both are recoverable if handled promptly.
enum class DecodeResult { OK, KV_FULL, ERROR };
DecodeResult safe_decode(llama_context* ctx, llama_batch& batch) {
int rc = llama_decode(ctx, batch);
if (rc == 0) return DecodeResult::OK;
if (rc == 1) {
fprintf(stderr, "KV cache full — triggering context shift\n");
// Shift context: remove oldest tokens
int n_ctx = llama_n_ctx(ctx);
int n_keep = 4; // preserve system prompt
int n_shift = n_ctx / 2;
llama_kv_self_seq_rm(ctx, 0, n_keep, n_keep + n_shift);
llama_kv_self_seq_add(ctx, 0, n_keep + n_shift, n_ctx, -n_shift);
return DecodeResult::KV_FULL;
}
fprintf(stderr, "Decode error: rc=%d\n", rc);
return DecodeResult::ERROR;
}
After a KV cache shift, you must also adjust your internal token tracking so future batches reference the correct positions. The shift effectively "forgets" the oldest half of the conversation.
Pattern 5: Retry with Exponential Backoff
For transient failures — particularly when llama.cpp is wrapped behind an HTTP server or used with mmap'd models on network filesystems — a retry loop with exponential backoff is essential.
#include <chrono>
#include <thread>
template <typename Fn>
auto retry_with_backoff(Fn fn, int max_attempts, int initial_delay_ms)
-> decltype(fn()) {
int delay = initial_delay_ms;
for (int attempt = 1; attempt <= max_attempts; ++attempt) {
try {
return fn();
} catch (const std::exception& e) {
fprintf(stderr, "Attempt %d/%d failed: %s\n",
attempt, max_attempts, e.what());
if (attempt == max_attempts) throw;
std::this_thread::sleep_for(
std::chrono::milliseconds(delay));
delay *= 2;
}
}
throw std::runtime_error("retry_with_backoff: unreachable");
}
Use this wrapper around model loading or any operation that touches external resources. Avoid retrying decode errors, since those indicate state corruption that retrying will not fix.
Pattern 6: Sampling Error Recovery
Grammar-constrained sampling can fail when the grammar is incompatible with the current token distribution. A fallback to unconstrained sampling prevents hard failures.
llama_token safe_sample(llama_context* ctx,
llama_sampler* constrained,
llama_sampler* fallback) {
llama_token token = llama_sampler_sample(constrained, ctx, -1);
// Detect invalid token (grammar rejection exhausted all candidates)
if (token == LLAMA_TOKEN_NULL) {
fprintf(stderr, "Constrained sampling failed — using fallback\n");
token = llama_sampler_sample(fallback, ctx, -1);
}
return token;
}
This is especially useful when using GBNF grammars for structured output. A malformed grammar rule can silently produce no valid tokens; the fallback ensures generation continues, even if the output is no longer strictly structured.
Pattern 7: Circuit Breaker for Repeated Failures
In a service context, repeated failures may indicate a systemic problem (corrupted model, exhausted memory). A circuit breaker prevents cascading failures by short-circuiting requests after a threshold is reached.
class CircuitBreaker {
int failure_count = 0;
int threshold;
std::chrono::steady_clock::time_point open_until;
std::chrono::seconds cooldown;
public:
CircuitBreaker(int thresh, std::chrono::seconds cd)
: threshold(thresh), cooldown(cd) {}
bool allow() {
if (failure_count < threshold) return true;
return std::chrono::steady_clock::now() >= open_until;
}
void record_failure() {
if (++failure_count >= threshold) {
open_until = std::chrono::steady_clock::now() + cooldown;
fprintf(stderr, "Circuit opened for %ld seconds\n",
cooldown.count());
}
}
void record_success() {
failure_count = 0;
}
};
Integrate the circuit breaker at the request boundary. When open, return a 503 response or a cached fallback answer instead of attempting inference.
Putting It All Together
The following example combines several patterns into a single resilient generation function:
struct GenerationResult {
std::string text;
bool truncated;
bool degraded;
std::string warning;
};
GenerationResult resilient_generate(llama_model* model,
llama_context* ctx,
const std::string& prompt,
int max_new_tokens) {
GenerationResult result;
// Pattern 3: safe tokenization with overflow recovery
std::vector<llama_token> tokens;
if (!safe_tokenize(model, prompt, tokens, llama_n_ctx(ctx), true)) {
result.warning = "Tokenization failed";
return result;
}
if ((int)tokens.size() > (int)prompt.size() / 2) {
result.truncated = true;
}
llama_batch batch = llama_batch_get_one(tokens.data(), tokens.size());
// Pattern 4: decode with KV recovery
auto dr = safe_decode(ctx, batch);
if (dr == DecodeResult::ERROR) {
result.warning = "Initial decode failed";
return result;
}
if (dr == DecodeResult::KV_FULL) {
result.degraded = true;
}
// Generation loop
llama_sampler* sampler = llama_sampler_chain_init(
llama_sampler_chain_default_params());
llama_sampler_chain_add(sampler,
llama_sampler_init_temp(0.8f));
llama_sampler_chain_add(sampler,
llama_sampler_init_dist(0));
for (int i = 0; i < max_new_tokens; ++i) {
llama_token tok = llama_sampler_sample(sampler, ctx, -1);
if (llama_token_is_eog(model, tok)) break;
char buf[16];
int n = llama_token_to_piece(model, tok, buf, sizeof(buf), 0, true);
if (n > 0) result.text.append(buf, n);
batch = llama_batch_get_one(&tok, 1);
dr = safe_decode(ctx, batch);
if (dr == DecodeResult::ERROR) {
result.warning = "Decode error during generation";
break;
}
if (dr == DecodeResult::KV_FULL) {
result.degraded = true;
}
}
llama_sampler_free(sampler);
return result;
}
The caller receives not only the generated text but also metadata about whether the response was truncated or degraded, enabling transparent reporting to end users.
Best Practices
- Always check return codes. Never assume
llama_decodeorllama_tokenizesucceeded. Silent failures produce garbage output that is harder to debug than crashes. - Use RAII for all resources. Wrap
llama_model,llama_context, andllama_samplerin smart-pointer-like classes to guarantee cleanup on all exit paths. - Log with context. Include token counts, batch sizes, and context window utilization in error messages. This data is invaluable for diagnosing intermittent failures.
- Validate input early. Check for empty strings, invalid UTF-8, and oversized prompts before touching the model. Cheap pre-checks prevent expensive failed decodes.
- Separate transient from permanent errors. Retry OOM and network errors; do not retry logic errors like mismatched batch dimensions.
- Test failure paths. Deliberately feed oversized prompts, corrupt model files, and zero-length inputs to verify your recovery code actually works.
- Monitor KV cache usage. Track
llama_kv_self_seq_pos_maxagainstn_ctxto predict overflow before it happens. - Provide user-visible degradation signals. When output is truncated or generated under fallback conditions, surface that information rather than presenting degraded output as authoritative.
Conclusion
Error recovery in llama.cpp is not optional for production deployments — it is the difference between a demo and a reliable product. By combining RAII resource management, context overflow detection, decode error handling, retry logic, sampling fallbacks, and circuit breakers, you can build applications that survive the messy realities of local LLM inference. The patterns in this guide are composable: start with guarded initialization and safe tokenization, then layer in decode recovery and circuit breaking as your reliability requirements grow. With these foundations in place, your llama.cpp applications will degrade gracefully under stress and recover automatically when conditions improve.