[{"data":1,"prerenderedAt":982},["ShallowReactive",2],{"navigation":3,"\u002Fdocs\u002Fconcepts\u002Ferror-handling-model":314,"\u002Fdocs\u002Fconcepts\u002Ferror-handling-model-surround":977},[4,38,86,149,176,219,257,273],{"title":5,"path":6,"stem":7,"children":8,"icon":37},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17,22,27,32],{"title":10,"path":6,"stem":7,"icon":11},"Getting started","i-lucide-flag",{"title":13,"path":14,"stem":15,"icon":16},"Modules overview","\u002Fdocs\u002Fgetting-started\u002Fmodules-overview","1.docs\u002F1.getting-started\u002F2.modules-overview","i-lucide-boxes",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F3.installation","i-lucide-download",{"title":23,"path":24,"stem":25,"icon":26},"License configuration","\u002Fdocs\u002Fgetting-started\u002Flicense-configuration","1.docs\u002F1.getting-started\u002F4.license-configuration","i-lucide-key-round",{"title":28,"path":29,"stem":30,"icon":31},"Your first app","\u002Fdocs\u002Fgetting-started\u002Ffirst-app","1.docs\u002F1.getting-started\u002F5.first-app","i-lucide-square-play",{"title":33,"path":34,"stem":35,"icon":36},"Project setup","\u002Fdocs\u002Fgetting-started\u002Fproject-setup","1.docs\u002F1.getting-started\u002F6.project-setup","i-lucide-package",false,{"title":39,"path":40,"stem":41,"children":42,"icon":45},"Core Concepts","\u002Fdocs\u002Fconcepts","1.docs\u002F2.concepts\u002F1.index",[43,46,51,56,61,66,71,76,81],{"title":44,"path":40,"stem":41,"icon":45},"Core concepts","i-lucide-book-open",{"title":47,"path":48,"stem":49,"icon":50},"Architecture","\u002Fdocs\u002Fconcepts\u002Farchitecture","1.docs\u002F2.concepts\u002F2.architecture","i-lucide-layers",{"title":52,"path":53,"stem":54,"icon":55},"Exactly-once semantics","\u002Fdocs\u002Fconcepts\u002Fexactly-once","1.docs\u002F2.concepts\u002F3.exactly-once","i-lucide-shield-check",{"title":57,"path":58,"stem":59,"icon":60},"Lanes and parallelism","\u002Fdocs\u002Fconcepts\u002Flanes-and-parallelism","1.docs\u002F2.concepts\u002F4.lanes-and-parallelism","i-lucide-split",{"title":62,"path":63,"stem":64,"icon":65},"State and thread-safety","\u002Fdocs\u002Fconcepts\u002Fstate-and-thread-safety","1.docs\u002F2.concepts\u002F5.state-and-thread-safety","i-lucide-lock",{"title":67,"path":68,"stem":69,"icon":70},"Event time and watermarks","\u002Fdocs\u002Fconcepts\u002Fevent-time-and-watermarks","1.docs\u002F2.concepts\u002F6.event-time-and-watermarks","i-lucide-clock",{"title":72,"path":73,"stem":74,"icon":75},"The configuration model","\u002Fdocs\u002Fconcepts\u002Fconfiguration-model","1.docs\u002F2.concepts\u002F7.configuration-model","i-lucide-sliders-horizontal",{"title":77,"path":78,"stem":79,"icon":80},"The error-handling model","\u002Fdocs\u002Fconcepts\u002Ferror-handling-model","1.docs\u002F2.concepts\u002F8.error-handling-model","i-lucide-triangle-alert",{"title":82,"path":83,"stem":84,"icon":85},"How StoatFlow differs from Kafka Streams","\u002Fdocs\u002Fconcepts\u002Fhow-stoatflow-differs-from-ks","1.docs\u002F2.concepts\u002F9.how-stoatflow-differs-from-ks","i-lucide-arrow-left-right",{"title":87,"path":88,"stem":89,"children":90,"icon":93},"Building Topologies","\u002Fdocs\u002Fbuilding","1.docs\u002F3.building\u002F1.index",[91,94,99,104,109,114,119,124,129,134,139,144],{"title":92,"path":88,"stem":89,"icon":93},"Building topologies","i-lucide-blocks",{"title":95,"path":96,"stem":97,"icon":98},"Error handling and DLQ","\u002Fdocs\u002Fbuilding\u002Ferror-handling-dlq","1.docs\u002F3.building\u002F10.error-handling-dlq","i-lucide-circle-x",{"title":100,"path":101,"stem":102,"icon":103},"State stores","\u002Fdocs\u002Fbuilding\u002Fstate-stores","1.docs\u002F3.building\u002F11.state-stores","i-lucide-database",{"title":105,"path":106,"stem":107,"icon":108},"Testing topologies","\u002Fdocs\u002Fbuilding\u002Ftesting","1.docs\u002F3.building\u002F12.testing","i-lucide-flask-conical",{"title":110,"path":111,"stem":112,"icon":113},"Sources and sinks","\u002Fdocs\u002Fbuilding\u002Fstreams-builder","1.docs\u002F3.building\u002F2.streams-builder","i-lucide-import",{"title":115,"path":116,"stem":117,"icon":118},"KStream and KTable operations","\u002Fdocs\u002Fbuilding\u002Fkstream-ktable","1.docs\u002F3.building\u002F3.kstream-ktable","i-lucide-waypoints",{"title":120,"path":121,"stem":122,"icon":123},"Aggregations","\u002Fdocs\u002Fbuilding\u002Faggregations","1.docs\u002F3.building\u002F4.aggregations","i-lucide-sigma",{"title":125,"path":126,"stem":127,"icon":128},"Windowing","\u002Fdocs\u002Fbuilding\u002Fwindowing","1.docs\u002F3.building\u002F5.windowing","i-lucide-calendar-clock",{"title":130,"path":131,"stem":132,"icon":133},"Joins","\u002Fdocs\u002Fbuilding\u002Fjoins","1.docs\u002F3.building\u002F6.joins","i-lucide-git-merge",{"title":135,"path":136,"stem":137,"icon":138},"The Processor API","\u002Fdocs\u002Fbuilding\u002Fprocessor-api","1.docs\u002F3.building\u002F7.processor-api","i-lucide-cpu",{"title":140,"path":141,"stem":142,"icon":143},"Scheduled sources","\u002Fdocs\u002Fbuilding\u002Fscheduled-sources","1.docs\u002F3.building\u002F8.scheduled-sources","i-lucide-timer",{"title":145,"path":146,"stem":147,"icon":148},"Serdes and Avro","\u002Fdocs\u002Fbuilding\u002Fserdes","1.docs\u002F3.building\u002F9.serdes","i-lucide-binary",{"title":150,"path":151,"stem":152,"children":153,"icon":75},"Configuration","\u002Fdocs\u002Fconfiguration","1.docs\u002F4.configuration\u002F1.index",[154,156,161,166,171],{"title":155,"path":151,"stem":152,"icon":75},"How configuration works",{"title":157,"path":158,"stem":159,"icon":160},"Engine configuration (:core)","\u002Fdocs\u002Fconfiguration\u002Fcore-config","1.docs\u002F4.configuration\u002F2.core-config","i-lucide-settings-2",{"title":162,"path":163,"stem":164,"icon":165},"Runtime configuration (:runtime)","\u002Fdocs\u002Fconfiguration\u002Fruntime-config","1.docs\u002F4.configuration\u002F3.runtime-config","i-lucide-server-cog",{"title":167,"path":168,"stem":169,"icon":170},"Defaults, adaptivity, and presets","\u002Fdocs\u002Fconfiguration\u002Fdefaults-and-presets","1.docs\u002F4.configuration\u002F4.defaults-and-presets","i-lucide-gauge",{"title":172,"path":173,"stem":174,"icon":175},"Kafka client configuration","\u002Fdocs\u002Fconfiguration\u002Fkafka-client-config","1.docs\u002F4.configuration\u002F5.kafka-client-config","i-lucide-plug",{"title":177,"path":178,"stem":179,"children":180,"icon":183},"Running in Production","\u002Fdocs\u002Fruntime","1.docs\u002F5.runtime\u002F1.index",[181,184,189,194,199,204,209,214],{"title":182,"path":178,"stem":179,"icon":183},"The runtime","i-lucide-server",{"title":185,"path":186,"stem":187,"icon":188},"The REST API","\u002Fdocs\u002Fruntime\u002Frest-api","1.docs\u002F5.runtime\u002F2.rest-api","i-lucide-globe",{"title":190,"path":191,"stem":192,"icon":193},"Health checks","\u002Fdocs\u002Fruntime\u002Fhealth-checks","1.docs\u002F5.runtime\u002F3.health-checks","i-lucide-heart-pulse",{"title":195,"path":196,"stem":197,"icon":198},"Metrics","\u002Fdocs\u002Fruntime\u002Fmetrics","1.docs\u002F5.runtime\u002F4.metrics","i-lucide-activity",{"title":200,"path":201,"stem":202,"icon":203},"Pause and resume","\u002Fdocs\u002Fruntime\u002Fpause-unpause","1.docs\u002F5.runtime\u002F5.pause-unpause","i-lucide-pause",{"title":205,"path":206,"stem":207,"icon":208},"Plugins and lifecycle hooks","\u002Fdocs\u002Fruntime\u002Fplugins","1.docs\u002F5.runtime\u002F6.plugins","i-lucide-puzzle",{"title":210,"path":211,"stem":212,"icon":213},"Docker images","\u002Fdocs\u002Fruntime\u002Fdocker","1.docs\u002F5.runtime\u002F7.docker","i-lucide-container",{"title":215,"path":216,"stem":217,"icon":218},"GraalVM native image","\u002Fdocs\u002Fruntime\u002Fnative-image","1.docs\u002F5.runtime\u002F8.native-image","i-lucide-zap",{"title":220,"path":221,"stem":222,"children":223,"icon":226},"Deploying & Operating","\u002Fdocs\u002Foperating","1.docs\u002F6.operating\u002F1.index",[224,227,232,237,242,247,252],{"title":225,"path":221,"stem":222,"icon":226},"Deploying and operating","i-lucide-life-buoy",{"title":228,"path":229,"stem":230,"icon":231},"Running on Kubernetes","\u002Fdocs\u002Foperating\u002Fkubernetes","1.docs\u002F6.operating\u002F2.kubernetes","i-lucide-ship",{"title":233,"path":234,"stem":235,"icon":236},"High availability","\u002Fdocs\u002Foperating\u002Fhigh-availability","1.docs\u002F6.operating\u002F3.high-availability","i-lucide-copy",{"title":238,"path":239,"stem":240,"icon":241},"Liveness and readiness probes","\u002Fdocs\u002Foperating\u002Fprobes","1.docs\u002F6.operating\u002F4.probes","i-lucide-stethoscope",{"title":243,"path":244,"stem":245,"icon":246},"Observability","\u002Fdocs\u002Foperating\u002Fobservability","1.docs\u002F6.operating\u002F5.observability","i-lucide-telescope",{"title":248,"path":249,"stem":250,"icon":251},"Tuning under load","\u002Fdocs\u002Foperating\u002Ftuning","1.docs\u002F6.operating\u002F6.tuning","i-lucide-sliders",{"title":253,"path":254,"stem":255,"icon":256},"Production checklist","\u002Fdocs\u002Foperating\u002Fproduction-checklist","1.docs\u002F6.operating\u002F7.production-checklist","i-lucide-clipboard-check",{"title":258,"path":259,"stem":260,"children":261,"icon":85},"Migrating from Kafka Streams","\u002Fdocs\u002Fmigration","1.docs\u002F7.migration\u002F1.index",[262,263,268],{"title":258,"path":259,"stem":260,"icon":85},{"title":264,"path":265,"stem":266,"icon":267},"Migration without carrying state","\u002Fdocs\u002Fmigration\u002Fwithout-data-migration","1.docs\u002F7.migration\u002F2.without-data-migration","i-lucide-sparkles",{"title":269,"path":270,"stem":271,"icon":272},"Migration carrying state","\u002Fdocs\u002Fmigration\u002Fwith-data-migration","1.docs\u002F7.migration\u002F3.with-data-migration","i-lucide-database-backup",{"title":274,"path":275,"stem":276,"children":277,"icon":279},"Reference","\u002Fdocs\u002Freference","1.docs\u002F8.reference\u002F1.index",[278,280,285,290,295,300,305,309],{"title":274,"path":275,"stem":276,"icon":279},"i-lucide-list",{"title":281,"path":282,"stem":283,"icon":284},"Configuration reference","\u002Fdocs\u002Freference\u002Fconfiguration-reference","1.docs\u002F8.reference\u002F2.configuration-reference","i-lucide-table",{"title":286,"path":287,"stem":288,"icon":289},"REST API reference","\u002Fdocs\u002Freference\u002Frest-api-reference","1.docs\u002F8.reference\u002F3.rest-api-reference","i-lucide-network",{"title":291,"path":292,"stem":293,"icon":294},"Gradle plugin reference","\u002Fdocs\u002Freference\u002Fgradle-plugin-reference","1.docs\u002F8.reference\u002F4.gradle-plugin-reference","i-lucide-box",{"title":296,"path":297,"stem":298,"icon":299},"Maven reference","\u002Fdocs\u002Freference\u002Fmaven-reference","1.docs\u002F8.reference\u002F5.maven-reference","i-simple-icons-apachemaven",{"title":301,"path":302,"stem":303,"icon":304},"Kafka Streams compatibility matrix","\u002Fdocs\u002Freference\u002Fks-compatibility-matrix","1.docs\u002F8.reference\u002F6.ks-compatibility-matrix","i-lucide-table-2",{"title":306,"path":307,"stem":308,"icon":170},"Metrics reference","\u002Fdocs\u002Freference\u002Fmetrics-reference","1.docs\u002F8.reference\u002F7.metrics-reference",{"title":310,"path":311,"stem":312,"icon":313},"Glossary","\u002Fdocs\u002Freference\u002Fglossary","1.docs\u002F8.reference\u002F8.glossary","i-lucide-book-a",{"id":315,"title":77,"body":316,"description":970,"extension":971,"meta":972,"navigation":973,"pageType":974,"path":78,"seo":975,"stem":79,"__hash__":976},"docs\u002F1.docs\u002F2.concepts\u002F8.error-handling-model.md",{"type":317,"value":318,"toc":954},"minimark",[319,340,355,416,421,428,433,440,444,464,468,471,485,500,504,511,588,594,611,617,639,643,650,668,671,689,706,713,717,720,737,744,752,756,769,784,787,805,809,818,907,920,924],[320,321,322,323,327,328,331,332,335,336,339],"p",{},"This page is the conceptual model for how a StoatFlow application behaves when something goes wrong with a record or a commit. It describes ",[324,325,326],"strong",{},"which failures are distinct",", ",[324,329,330],{},"the policies you can apply to each",", and ",[324,333,334],{},"what the runtime does that you don't get to configure"," — the commit failure that ends in a process restart. The concrete handler classes and the configuration keys that select them live in ",[337,338,95],"a",{"href":96},"; this page stays at the level of behaviour.",[320,341,342,343,345,346,349,350,354],{},"The starting point is the same single-process model the rest of these pages build on. Because there's one instance and one transaction per commit (see ",[337,344,47],{"href":48}," and ",[337,347,348],{"href":53},"Exactly-once","), every failure either gets handled ",[351,352,353],"em",{},"inside"," an epoch — without disturbing the exactly-once guarantee — or it's fatal and the epoch is thrown away. There's no middle ground where a node limps along in a degraded state.",[356,357,358],"tldr-panel",{},[359,360,361,380,398,404,410],"ul",{},[362,363,364,367,368,371,372,375,376,379],"li",{},[324,365,366],{},"Three failure classes, each with its own policy:"," a record fails to ",[324,369,370],{},"deserialize"," on input, a ",[324,373,374],{},"processor"," throws while handling a record, or ",[324,377,378],{},"production"," (serialize + send) fails on output.",[362,381,382,385,386,389,390,393,394,397],{},[324,383,384],{},"Three policies per class:"," ",[351,387,388],{},"log and continue"," (skip the record), ",[351,391,392],{},"log and fail"," (stop the topology), or ",[351,395,396],{},"dead-letter"," (route the record and its error context to a DLQ topic).",[362,399,400,403],{},[324,401,402],{},"DLQ records are part of the transaction:"," they commit on the same barrier as everything else, so a dead-lettered record is never lost and never duplicated.",[362,405,406,409],{},[324,407,408],{},"Producer errors are classified"," retriable vs fatal: transient send errors retry with backoff; fatal ones end the epoch.",[362,411,412,415],{},[324,413,414],{},"A failed commit is not configurable:"," the in-flight transaction aborts, the process exits, and the orchestrator restarts it from the last successful barrier.",[417,418,420],"h2",{"id":419},"three-classes-of-failure","Three classes of failure",[320,422,423,424,427],{},"The runtime distinguishes failures by ",[351,425,426],{},"where in the record's lifecycle"," they happen. Each class is independent — you can log-and-continue on bad input while still failing hard on a processor bug, or vice versa.",[429,430,432],"h3",{"id":431},"deserialization-on-the-way-in","Deserialization (on the way in)",[320,434,435,436,439],{},"A record arrives from Kafka as raw bytes. Before the topology can touch it, the key and value bytes must deserialize through the configured serdes. If that fails — malformed JSON, a schema the deserializer can't read, a corrupt payload — it's a ",[324,437,438],{},"deserialization failure",". The topology never ran for this record; there's nothing to roll back. This is the failure class for source topics that may carry records your app can't read (a producer upstream changed format, a poison record slipped in).",[429,441,443],{"id":442},"processing-inside-the-topology","Processing (inside the topology)",[320,445,446,447,451,452,455,456,459,460,463],{},"The record deserialized fine and entered the topology, and then a processor threw — a ",[448,449,450],"code",{},"NullPointerException"," in a ",[448,453,454],{},"mapValues",", a validation error in a custom ",[448,457,458],{},"Processor",", an unchecked exception from an enrichment call. This is a ",[324,461,462],{},"processing failure",": it happens on a processing lane, mid-DAG, after the record was already accepted. Whatever state the record touched before the throw is part of the current epoch and is governed by the same commit barrier as everything else — a skipped or dead-lettered record doesn't leak half-applied state into the committed snapshot.",[429,465,467],{"id":466},"production-on-the-way-out","Production (on the way out)",[320,469,470],{},"The topology produced an output record and the runtime tried to publish it. Two things can go wrong here, and the runtime treats them as one class with sub-cases:",[359,472,473,479],{},[362,474,475,478],{},[324,476,477],{},"Serialization"," — the output key or value can't be turned into bytes (a serde rejects the value, a Schema Registry call fails). This is deterministic: the same record will fail the same way every time, so it's never retried.",[362,480,481,484],{},[324,482,483],{},"Send"," — serialization succeeded but the producer couldn't deliver (broker unreachable, request timed out, an authorization error). Some send failures are transient and worth retrying; others are permanent.",[320,486,487,488,491,492,495,496,499],{},"This is a ",[324,489,490],{},"production failure",". It's the only class where ",[351,493,494],{},"retry"," is a meaningful policy, because it's the only class where the failure might be transient — see ",[351,497,498],{},"Producer error classification"," below.",[417,501,503],{"id":502},"the-handler-model-continue-fail-or-dead-letter","The handler model: continue, fail, or dead-letter",[320,505,506,507,510],{},"Each failure class is governed by an ",[324,508,509],{},"exception handler"," — a policy the runtime consults when that class of failure occurs. There are three behaviours a handler can express, and the built-in handlers give you one each:",[512,513,514,530],"table",{},[515,516,517],"thead",{},[518,519,520,524,527],"tr",{},[521,522,523],"th",{},"Policy",[521,525,526],{},"What it does",[521,528,529],{},"When it fits",[531,532,533,551,567],"tbody",{},[518,534,535,541,548],{},[536,537,538],"td",{},[324,539,540],{},"Log and continue",[536,542,543,544,547],{},"Logs the error and ",[324,545,546],{},"skips"," the offending record. Processing carries on with the next record.",[536,549,550],{},"Lossy-tolerant streams where one bad record shouldn't stop the pipeline and you don't need to recover the record later.",[518,552,553,558,564],{},[536,554,555],{},[324,556,557],{},"Log and fail",[536,559,543,560,563],{},[324,561,562],{},"stops"," the topology. The process shuts down.",[536,565,566],{},"Correctness-critical streams where a bad record means something is wrong upstream that a human needs to look at. The conservative default.",[518,568,569,574,581],{},[536,570,571],{},[324,572,573],{},"Dead-letter (DLQ)",[536,575,576,577,580],{},"Routes the record and its error context to a configured ",[324,578,579],{},"dead-letter topic",", then continues.",[536,582,583,584,587],{},"You want to keep processing ",[351,585,586],{},"and"," keep the bad records for offline inspection or replay.",[320,589,590,591,593],{},"The defaults are deliberately strict. Deserialization and processing both default to ",[324,592,392],{}," — silently skipping records should be a choice you make on purpose, not a behaviour you inherit. Production defaults to a handler that retries transient send errors and fails on the rest. You opt into continue-or-dead-letter behaviour per class; the building guide shows how the handlers are named and wired.",[595,596,599],"callout",{"color":597,"icon":598},"warning","i-lucide-shield",[320,600,601,385,604,606,607,610],{},[324,602,603],{},"Skipping is data loss with a log line.",[351,605,540],{}," drops the record and moves on — the only trace is a log entry and an error-metric tick on ",[448,608,609],{},"\u002Fmetrics",". If you might ever need the record back, choose the DLQ policy instead, where the bytes are preserved on a topic you control.",[320,612,613,614,616],{},"These handler classes follow the shape of Kafka Streams' KIP-1033 (processing) and KIP-1034 (deserialization \u002F production \u002F DLQ) error-handling design, so the mental model carries over directly if you know Kafka Streams. The behavioural deltas are catalogued in the ",[337,615,301],{"href":302},".",[320,618,619,620,623,624,626,627,630,631,634,635,638],{},"One escape hatch sits above the per-class policies. A ",[351,621,622],{},"failed"," record error (the ",[324,625,392],{}," outcome) surfaces as a fatal stream error, and by default it stops the instance. An application can register the KS-compatible ",[448,628,629],{},"StreamsUncaughtExceptionHandler","; returning ",[448,632,633],{},"REPLACE_THREAD"," makes the runtime recover with an ",[324,636,637],{},"in-place engine restart"," — the processing engine is torn down and rebuilt inside the same process, resuming from the last committed barrier with the interrupted epoch aborted, exactly as a crash recovery would. It's the single-instance analogue of Kafka Streams replacing a failed stream thread, and it's bounded: a restart budget shuts the application down if faults keep recurring.",[417,640,642],{"id":641},"dead-letter-queue-semantics","Dead-letter queue semantics",[320,644,645,646,649],{},"A DLQ handler doesn't publish to the dead-letter topic out of band. It hands the failed record back to the runtime, and that record is sent through ",[324,647,648],{},"the same transactional producer, on the same commit barrier"," as the epoch's normal output. This is the property that makes the DLQ trustworthy:",[359,651,652,658],{},[362,653,654,657],{},[324,655,656],{},"No loss."," The dead-lettered record commits atomically with the rest of the epoch. If the epoch commits, the DLQ record is on the topic; if the epoch aborts (a crash mid-barrier), the DLQ record is discarded along with everything else and the source record will be re-read and re-handled after restart.",[362,659,660,663,664,667],{},[324,661,662],{},"No duplicates."," Because it rides the exactly-once transaction, a dead-lettered record appears on the DLQ topic exactly once for downstream consumers reading with ",[448,665,666],{},"read_committed"," isolation — same guarantee as your real output.",[320,669,670],{},"What each DLQ record carries depends on the failure class:",[359,672,673,683],{},[362,674,675,678,679,682],{},[324,676,677],{},"Deserialization DLQ records"," preserve the ",[324,680,681],{},"original raw key\u002Fvalue bytes"," — nothing was successfully deserialized, so the untouched payload is exactly what you need to diagnose or replay it.",[362,684,685,688],{},[324,686,687],{},"Processing and production DLQ records"," carry the record as it was at the point of failure, plus error-context metadata.",[320,690,691,692,695,696,699,700,703,704,616],{},"Every DLQ record is annotated with ",[324,693,694],{},"error-context headers"," under a StoatFlow-specific ",[448,697,698],{},"__stoatflow.errors.*"," namespace (KIP-1034-shaped): the failure type, the exception class and message, an optional stack trace, the originating source topic \u002F partition \u002F offset, and which component failed. DLQ tooling that filters out the conventional ",[448,701,702],{},"__","-prefixed internal headers must opt in to surface these. The exact header set is documented in ",[337,705,95],{"href":96},[595,707,710],{"color":708,"icon":709},"info","i-lucide-info",[320,711,712],{},"A dead-letter topic is an ordinary Kafka topic that you create and own. The runtime only produces to it — it doesn't read it back. Replaying dead-lettered records into the source (after you've fixed the upstream cause) is your call to make, with your tooling.",[417,714,716],{"id":715},"producer-error-classification-retriable-vs-fatal","Producer error classification: retriable vs fatal",[320,718,719],{},"Production is the one failure class where the runtime makes a finer distinction, because a send failure might be transient. When the producer reports an error, the runtime classifies it before deciding what to do:",[359,721,722,731],{},[362,723,724,727,728,730],{},[324,725,726],{},"Retriable."," Transient conditions — a request timeout, a momentarily unreachable broker — that may succeed if tried again. The default production handler returns ",[351,729,494],{},", and the runtime retries with exponential backoff for a bounded number of attempts before giving up. A short broker hiccup self-heals; throughput dips and recovers.",[362,732,733,736],{},[324,734,735],{},"Fatal."," Conditions that won't improve by retrying — a serialization error (deterministic, the same record fails the same way), an authentication or authorization failure, a broker-incompatibility error, or a producer-fencing condition. These can't be handled away inside the epoch, so they end it.",[320,738,739,740,743],{},"The serialization-vs-send split matters here: serialization failures are ",[351,741,742],{},"always"," classified non-retriable (retrying can't change a deterministic outcome), while send failures are classified case by case. A fatal production error that has a DLQ configured will still emit the DLQ record before the epoch ends, so even a fatal failure leaves a forensic trail.",[320,745,746,747,751],{},"The classification rules are stricter than Kafka Streams' in one specific way: StoatFlow has a single instance and no task to migrate, so the failure conditions Kafka Streams would resolve by moving a task elsewhere have nowhere to go and are treated as fatal. That's a direct consequence of the ",[337,748,750],{"href":749},"\u002Fdocs\u002Fconcepts\u002Farchitecture#the-single-instance-model","single-instance model",", not a separate design choice.",[417,753,755],{"id":754},"when-the-commit-itself-fails","When the commit itself fails",[320,757,758,759,761,762,765,766,616],{},"Everything above happens ",[351,760,353],{}," an epoch and leaves the exactly-once guarantee intact. There is one failure the runtime does ",[324,763,764],{},"not"," hand to a configurable policy: the ",[324,767,768],{},"commit barrier itself failing",[320,770,771,772,776,777,780,781,783],{},"When the per-epoch Kafka transaction can't complete — it times out, the broker rejects it, the producer gets fenced — there is no safe way to continue. The runtime aborts the in-flight transaction and the process exits. As described on the ",[337,773,775],{"href":774},"\u002Fdocs\u002Fconcepts\u002Farchitecture#exactly-once-semantics-the-commit-barrier","architecture page",", the aborted transaction's partial work — uncommitted state writes, uncommitted output, uncommitted offset advances — is discarded at the broker. Your orchestrator (Kubernetes, typically) restarts the process, which resumes from the ",[324,778,779],{},"last successful barrier"," and re-reads every record after it. Downstream consumers reading with ",[448,782,666],{}," isolation never see the aborted epoch's output.",[320,785,786],{},"This is intentional and not tunable: a single-instance, single-transaction design has no notion of \"commit failed but keep going.\" A stalled or failed commit is treated as a fatal condition, the epoch is thrown away whole, and recovery is a clean restart from a known-good point. The same restart-on-fatal pattern covers a sustained broker outage that exhausts the retry budget — the runtime stops making progress, the process exits, and the orchestrator brings it back. The internal protocol that detects a stalled commit and converts it into this abort-and-exit behaviour stays in the source; what you observe is the restart.",[595,788,789],{"color":708,"icon":193},[320,790,791,792,795,796,799,800,804],{},"A restart is the recovery path, not an outage you have to engineer around. High availability under this model comes from ",[324,793,794],{},"fast restart"," — or, with an opt-in ",[337,797,798],{"href":234},"hot standby",", failover to a warm passive peer — see the ",[337,801,803],{"href":802},"\u002Fdocs\u002Fconcepts\u002Farchitecture#lifecycle-startup-restart-recovery","lifecycle and recovery"," section. The readiness probe stays down while the restarting instance restores state, so traffic and load balancers wait until it's caught up.",[417,806,808],{"id":807},"what-you-observe","What you observe",[320,810,811,812,815,816,616],{},"Each failure class surfaces the same way you'd expect from the architecture page's operational framing — a metric signal, a log line, and (for DLQ policies) the records themselves. The column below names the ",[351,813,814],{},"signal",", not the exact Prometheus metric ID; the precise names are what you'll see exposed on ",[448,817,609],{},[512,819,820,836],{},[515,821,822],{},[518,823,824,827,830,833],{},[521,825,826],{},"Failure",[521,828,829],{},"Signal",[521,831,832],{},"Log",[521,834,835],{},"Record disposition",[531,837,838,852,866,880,893],{},[518,839,840,843,846,849],{},[536,841,842],{},"Deserialization",[536,844,845],{},"deserialization-error counter",[536,847,848],{},"error\u002Fwarn with topic-partition-offset",[536,850,851],{},"skipped, failed, or on the DLQ with original bytes",[518,853,854,857,860,863],{},[536,855,856],{},"Processing",[536,858,859],{},"processing-error counter",[536,861,862],{},"error\u002Fwarn with processor name + record metadata",[536,864,865],{},"skipped, failed, or on the DLQ",[518,867,868,871,874,877],{},[536,869,870],{},"Production (retriable)",[536,872,873],{},"Kafka-client error metrics",[536,875,876],{},"warn (\"will retry\")",[536,878,879],{},"retried with backoff",[518,881,882,885,887,890],{},[536,883,884],{},"Production (fatal)",[536,886,873],{},[536,888,889],{},"error",[536,891,892],{},"DLQ if configured, then the epoch ends",[518,894,895,898,901,904],{},[536,896,897],{},"Commit failure",[536,899,900],{},"commit-stall counter",[536,902,903],{},"error + thread dump",[536,905,906],{},"epoch aborted at broker; process restarts",[320,908,909,910,327,912,915,916,616],{},"Steady-state failure diagnosis uses the always-on admin surface — ",[448,911,609],{},[448,913,914],{},"\u002Fhealth\u002Fready",", and the debug endpoints — covered in the ",[337,917,919],{"href":918},"\u002Fdocs\u002Fconcepts\u002Farchitecture#failure-modes-and-observability","architecture page's observability section",[417,921,923],{"id":922},"where-to-go-next","Where to go next",[359,925,926,933,940,947],{},[362,927,928,932],{},[324,929,930],{},[337,931,95],{"href":96}," — the concrete handler classes, the configuration keys that select them per failure class, the full DLQ header set, and worked examples in Kotlin and Java.",[362,934,935,939],{},[324,936,937],{},[337,938,348],{"href":53}," — why DLQ records and skipped-record offsets commit atomically with the rest of the epoch.",[362,941,942,946],{},[324,943,944],{},[337,945,47],{"href":918}," — the full failure-mode and observability catalogue at the system level.",[362,948,949,953],{},[324,950,951],{},[337,952,301],{"href":302}," — how StoatFlow's error-handling surface lines up against Kafka Streams.",{"title":955,"searchDepth":956,"depth":956,"links":957},"",2,[958,964,965,966,967,968,969],{"id":419,"depth":956,"text":420,"children":959},[960,962,963],{"id":431,"depth":961,"text":432},3,{"id":442,"depth":961,"text":443},{"id":466,"depth":961,"text":467},{"id":502,"depth":956,"text":503},{"id":641,"depth":956,"text":642},{"id":715,"depth":956,"text":716},{"id":754,"depth":956,"text":755},{"id":807,"depth":956,"text":808},{"id":922,"depth":956,"text":923},"How StoatFlow classifies failures — deserialization, processing, and production — the skip \u002F fail \u002F dead-letter policies you choose, DLQ semantics, and the commit failure that ends in a restart.","md",{},{"icon":80},"explanation",{"title":77,"description":970},"Lzky-yh0LiO7d5v8e-RBQ94IEbIy22xuenQD7_ThyVw",[978,980],{"title":72,"path":73,"stem":74,"description":979,"icon":75,"children":-1},"How StoatFlow layers configuration — application.yaml, overlay files, environment variables, and programmatic overrides — the precedence order between them, and why some behaviour is adaptive rather than configured.",{"title":82,"path":83,"stem":84,"description":981,"icon":85,"children":-1},"The conceptual deltas if you already know Kafka Streams — single instance instead of a rebalancing cluster, in-memory re-keying instead of repartition topics, barrier-based exactly-once, global state, and lanes instead of partition-bound tasks. The DSL carries over unchanged.",1783713352419]