[{"data":1,"prerenderedAt":3542},["ShallowReactive",2],{"navigation":3,"\u002Fdocs\u002Foperating\u002Fhigh-availability":314,"\u002Fdocs\u002Foperating\u002Fhigh-availability-surround":3537},[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":233,"body":316,"description":3530,"extension":3531,"meta":3532,"navigation":3533,"pageType":3534,"path":234,"seo":3535,"stem":235,"__hash__":3536},"docs\u002F1.docs\u002F6.operating\u002F3.high-availability.md",{"type":317,"value":318,"toc":3515},"minimark",[319,332,382,387,447,464,472,476,495,513,516,543,546,552,697,703,741,744,832,836,843,864,871,874,885,1162,1259,1273,1278,1309,1312,1333,1348,1359,1479,1483,1500,1588,2203,2224,2228,2231,2276,2284,2293,2297,2303,2396,2416,2953,2975,3008,3057,3068,3075,3079,3086,3309,3329,3333,3354,3384,3391,3395,3476,3480,3511],[320,321,322,323,327,328,331],"p",{},"StoatFlow gives you two ways to stay available, and you choose per application. By default an application is a single instance that recovers by ",[324,325,326],"strong",{},"restarting"," — Kubernetes brings the pod back and state rebuilds from the changelog. For workloads that can't absorb that restart window, you can opt into a ",[324,329,330],{},"hot standby",": a cluster of one active instance plus one or more warm standbys, where a standby takes over in seconds. The single-instance default is unchanged; hot standby is an opt-in upgrade on top of it.",[333,334,335],"tldr-panel",{},[336,337,338,350,360,366,372],"ul",{},[339,340,341,344,345,349],"li",{},[324,342,343],{},"Two tiers:"," fast restart (the default, one instance) or an opt-in hot standby (",[346,347,348],"code",{},"stoatflow.ha.mode: active-standby",", one active plus one or more warm standbys).",[339,351,352,355,356,359],{},[324,353,354],{},"Default is unchanged:"," with ",[346,357,358],{},"mode: off",", recovery is a restart plus a changelog restore. Most deployments need nothing more.",[339,361,362,365],{},[324,363,364],{},"Hot standby:"," each passive standby continuously follows the changelog and stays warm, so promotion is seconds — independent of how large your state is.",[339,367,368,371],{},[324,369,370],{},"Still one active:"," at most one instance processes and commits at a time; the standbys are passive. On failover a lag-aware election promotes the freshest one, and under exactly-once Kafka's transactional fencing makes split-brain impossible.",[339,373,374,377,378,381],{},[324,375,376],{},"Shape:"," a ",[346,379,380],{},"StatefulSet"," with two or more replicas and split health probes; a rolling deploy costs about one graceful handoff, and a graceful role swap no longer restarts a pod.",[383,384,386],"h2",{"id":385},"the-two-ha-tiers","The two HA tiers",[388,389,390,409],"table",{},[391,392,393],"thead",{},[394,395,396,400,403,406],"tr",{},[397,398,399],"th",{},"Tier",[397,401,402],{},"What you get",[397,404,405],{},"Failover time",[397,407,408],{},"When to choose",[410,411,412,430],"tbody",{},[394,413,414,421,424,427],{},[415,416,417,420],"td",{},[324,418,419],{},"Fast restart"," (default)",[415,422,423],{},"One instance. On failure Kubernetes restarts the pod and state rebuilds from the changelog.",[415,425,426],{},"Restart + state restore — scales with state size.",[415,428,429],{},"Most workloads, where a restart-window gap is acceptable.",[394,431,432,438,441,444],{},[415,433,434,437],{},[324,435,436],{},"Hot standby"," (opt-in)",[415,439,440],{},"One active plus one or more warm standbys. Each standby follows the changelog; on failover the freshest is elected and takes over.",[415,442,443],{},"Seconds, independent of state size.",[415,445,446],{},"Large state (tens of GB) or a tight recovery-time objective where a cold restore is too slow.",[320,448,449,450,453,454,456,457,463],{},"Hot standby adds availability redundancy without changing the processing model, the exactly-once guarantees, or the single-active-instance shape. Everything the ",[451,452,47],"a",{"href":48}," page describes still holds; you're keeping one or more warm spares ready, nothing more. The simplest cluster is one active and one standby — a two-replica ",[346,455,380],{},", the default; add standbys for more redundancy (see ",[451,458,460],{"href":459},"#configuration",[346,461,462],{},"desired-standbys",").",[465,466,469],"callout",{"color":467,"icon":468},"info","i-lucide-info",[320,470,471],{},"If a cold-restore window of seconds-to-minutes is acceptable for your workload, stay on the default. Hot standby exists to remove that window for large-state or tight-RTO applications — not because the single-instance default is unsafe. Stateless topologies gain nothing from it; use a plain rolling restart.",[383,473,475],{"id":474},"how-hot-standby-works","How hot standby works",[320,477,478,479,482,483,486,487,490,491,494],{},"With ",[346,480,481],{},"ha.mode: active-standby"," you run two or more instances of the same application. At any moment one is the ",[324,484,485],{},"active"," — it owns the source partitions, processes records, and commits — and the rest are ",[324,488,489],{},"passive standbys",". Each standby consumes the same changelog the active writes, keeping its own local state warm and a step behind, but it does not process source records or produce output. They coordinate through a compacted internal Kafka topic (auto-created; its name is configurable via ",[346,492,493],{},"stoatflow.ha.coordination-topic","), exchanging a periodic heartbeat so each knows who is alive and which role they hold.",[320,496,497,500,501,504,505,508,509,512],{},[324,498,499],{},"How the standby stays warm."," A single changelog applier streams committed deltas into local state in real time — one consumer (in ",[346,502,503],{},"assign()"," mode, ",[346,506,507],{},"read_committed"," under exactly-once \u002F ",[346,510,511],{},"read_uncommitted"," under at-least-once) and one virtual thread. It applies uniformly across RocksDB and in-memory stores and folds the derived-index rebuilds for foreign-key joins and suppression into the same loop, so the standby's state — primary stores and derived indices alike — tracks the active continuously rather than in periodic batches. Replication lag is read straight from that consumer's position, with no admin round-trip on the broker.",[320,514,515],{},"Promotion happens two ways:",[336,517,518,537],{},[339,519,520,523,524,527,528,531,532,536],{},[324,521,522],{},"Graceful handoff"," — a rolling deploy, or an explicit ",[346,525,526],{},"POST \u002Fha\u002Fswitch",". The active drains its in-flight work, commits, and hands the role over to an elected standby. This is an ",[324,529,530],{},"in-place"," role swap: the demoted active rebuilds its engine as a standby ",[533,534,535],"em",{},"inside the same pod"," and rejoins warm — no process exit, no cold state open, no pod restart.",[339,538,539,542],{},[324,540,541],{},"Failover"," — the active stops heart-beating; the standbys notice, elect a successor, and it promotes itself.",[320,544,545],{},"Because the standby is already warm, taking over is a matter of seconds rather than a full cold restore. (A crash, a lost node, or a genuinely stuck restore still restarts the pod — the fence protects every path.)",[320,547,548,549,551],{},"The graceful path end to end — here triggered by an explicit ",[346,550,526],{},"; a rolling deploy runs the same sequence per pod:",[553,554,559],"pre",{"className":555,"code":556,"language":557,"meta":558,"style":558},"language-mermaid shiki shiki-themes vitesse-light","sequenceDiagram\n    autonumber\n    participant O as Operator\n    participant A as Pod A (active)\n    participant B as Pod B (standby)\n    participant K as Kafka\n\n    O->>A: POST \u002Fha\u002Fswitch\n    break no caught-up standby (and no force=true)\n        A-->>O: 409 Conflict\n    end\n    A-->>O: 202 Accepted (commandOffset)\n    A->>K: publish SWITCH on coordination topic\n    A->>K: drain in-flight epoch, commit final barrier\n    A->>A: rebuild engine as standby (in place, no pod restart)\n    note over A: keeps heartbeating as\u003Cbr\u002F>ACTIVE (DRAINING)\n    B->>K: claim promotion token (CAS)\n    note right of B: allowed — a draining\u003Cbr\u002F>holder is unprotected\n    B->>K: fence producer epoch\n    B->>K: drain changelog tail to LSO (already warm — near zero)\n    B->>K: heartbeat ACTIVE\n    A->>A: sees peer ACTIVE — becomes STANDBY\n","mermaid","",[346,560,561,570,576,582,588,594,600,607,613,619,625,631,637,643,649,655,661,667,673,679,685,691],{"__ignoreMap":558},[562,563,566],"span",{"class":564,"line":565},"line",1,[562,567,569],{"class":568},"suHK_","sequenceDiagram\n",[562,571,573],{"class":564,"line":572},2,[562,574,575],{"class":568},"    autonumber\n",[562,577,579],{"class":564,"line":578},3,[562,580,581],{"class":568},"    participant O as Operator\n",[562,583,585],{"class":564,"line":584},4,[562,586,587],{"class":568},"    participant A as Pod A (active)\n",[562,589,591],{"class":564,"line":590},5,[562,592,593],{"class":568},"    participant B as Pod B (standby)\n",[562,595,597],{"class":564,"line":596},6,[562,598,599],{"class":568},"    participant K as Kafka\n",[562,601,603],{"class":564,"line":602},7,[562,604,606],{"emptyLinePlaceholder":605},true,"\n",[562,608,610],{"class":564,"line":609},8,[562,611,612],{"class":568},"    O->>A: POST \u002Fha\u002Fswitch\n",[562,614,616],{"class":564,"line":615},9,[562,617,618],{"class":568},"    break no caught-up standby (and no force=true)\n",[562,620,622],{"class":564,"line":621},10,[562,623,624],{"class":568},"        A-->>O: 409 Conflict\n",[562,626,628],{"class":564,"line":627},11,[562,629,630],{"class":568},"    end\n",[562,632,634],{"class":564,"line":633},12,[562,635,636],{"class":568},"    A-->>O: 202 Accepted (commandOffset)\n",[562,638,640],{"class":564,"line":639},13,[562,641,642],{"class":568},"    A->>K: publish SWITCH on coordination topic\n",[562,644,646],{"class":564,"line":645},14,[562,647,648],{"class":568},"    A->>K: drain in-flight epoch, commit final barrier\n",[562,650,652],{"class":564,"line":651},15,[562,653,654],{"class":568},"    A->>A: rebuild engine as standby (in place, no pod restart)\n",[562,656,658],{"class":564,"line":657},16,[562,659,660],{"class":568},"    note over A: keeps heartbeating as\u003Cbr\u002F>ACTIVE (DRAINING)\n",[562,662,664],{"class":564,"line":663},17,[562,665,666],{"class":568},"    B->>K: claim promotion token (CAS)\n",[562,668,670],{"class":564,"line":669},18,[562,671,672],{"class":568},"    note right of B: allowed — a draining\u003Cbr\u002F>holder is unprotected\n",[562,674,676],{"class":564,"line":675},19,[562,677,678],{"class":568},"    B->>K: fence producer epoch\n",[562,680,682],{"class":564,"line":681},20,[562,683,684],{"class":568},"    B->>K: drain changelog tail to LSO (already warm — near zero)\n",[562,686,688],{"class":564,"line":687},21,[562,689,690],{"class":568},"    B->>K: heartbeat ACTIVE\n",[562,692,694],{"class":564,"line":693},22,[562,695,696],{"class":568},"    A->>A: sees peer ACTIVE — becomes STANDBY\n",[320,698,699,702],{},[324,700,701],{},"Drain-to-LSO before processing."," Whatever the trigger, a promoting pod first drains its applier to the committed end of the changelog — the log-stable-offset (LSO) — before it processes a single record. The departing active's producer is fenced eagerly, which freezes the LSO, so there is a fixed, finite target to drain to and no new committed deltas can appear past it. A caught-up standby drains almost nothing (it's already current); a standby that had fallen behind drains exactly the records it was missing — so it never resumes processing on stale state. This is what makes failover correct under exactly-once even when the standby was lagging at the moment the active died.",[320,704,705,708,709,712,713,716,717,720,721,724,725,728,729,732,733,736,737,740],{},[324,706,707],{},"Electing the successor."," With more than one standby, promotion is an election, not a free-for-all. Candidates are ranked by replication lag: a standby is eligible only while its total committed-changelog lag is at or below ",[346,710,711],{},"acceptable-recovery-lag",", and the ",[324,714,715],{},"freshest"," eligible standby wins — an exact tie broken by a stable hash of the pod identity (nudged by ",[346,718,719],{},"failover-priority","), so the choice always converges on exactly one. The winner claims a ",[324,722,723],{},"promotion token"," on the coordination topic ",[533,726,727],{},"before"," it fences the old active and restores; a standby that loses the claim simply ",[324,730,731],{},"stands down"," and stays a standby — no fence, no error, no restart. That ordering — elect and claim, ",[533,734,735],{},"then"," fence — is what stops two pods promoting at once in a multi-standby cluster. If the active is gone and no standby is within the threshold, the least-behind one is elected anyway after ",[346,738,739],{},"promotion-grace-ms"," (availability first) — still draining to the LSO before it processes. The producer-epoch fence stays the backstop: even if the election ever misfired, the broker still lets only one instance commit.",[320,742,743],{},"The same machinery under a crash — ungraceful failover from detection to takeover:",[553,745,747],{"className":555,"code":746,"language":557,"meta":558,"style":558},"sequenceDiagram\n    autonumber\n    participant A as Pod A (active)\n    participant B as Pod B (standby, freshest)\n    participant C as Pod C (standby, behind)\n    participant K as Kafka\n\n    A--xK: heartbeats stop (crash)\n    note over B,C: peer goes stale after the miss debounce\u003Cbr\u002F>(local-receipt freshness, not the sender's clock)\n    B->>B: election — lag-ranked, freshest eligible wins\n    C->>C: same election — not the winner, stands down\n    B->>K: claim promotion token (CAS)\n    K-->>B: token won\n    B->>B: settle window — confirm sole claimant\n    B->>K: fence producer epoch (freezes the LSO)\n    B->>K: drain changelog to LSO (only what it was missing)\n    B->>K: heartbeat ACTIVE\n    note over B: processing resumes on caught-up state\n",[346,748,749,753,757,761,766,771,775,779,784,789,794,799,803,808,813,818,823,827],{"__ignoreMap":558},[562,750,751],{"class":564,"line":565},[562,752,569],{"class":568},[562,754,755],{"class":564,"line":572},[562,756,575],{"class":568},[562,758,759],{"class":564,"line":578},[562,760,587],{"class":568},[562,762,763],{"class":564,"line":584},[562,764,765],{"class":568},"    participant B as Pod B (standby, freshest)\n",[562,767,768],{"class":564,"line":590},[562,769,770],{"class":568},"    participant C as Pod C (standby, behind)\n",[562,772,773],{"class":564,"line":596},[562,774,599],{"class":568},[562,776,777],{"class":564,"line":602},[562,778,606],{"emptyLinePlaceholder":605},[562,780,781],{"class":564,"line":609},[562,782,783],{"class":568},"    A--xK: heartbeats stop (crash)\n",[562,785,786],{"class":564,"line":615},[562,787,788],{"class":568},"    note over B,C: peer goes stale after the miss debounce\u003Cbr\u002F>(local-receipt freshness, not the sender's clock)\n",[562,790,791],{"class":564,"line":621},[562,792,793],{"class":568},"    B->>B: election — lag-ranked, freshest eligible wins\n",[562,795,796],{"class":564,"line":627},[562,797,798],{"class":568},"    C->>C: same election — not the winner, stands down\n",[562,800,801],{"class":564,"line":633},[562,802,666],{"class":568},[562,804,805],{"class":564,"line":639},[562,806,807],{"class":568},"    K-->>B: token won\n",[562,809,810],{"class":564,"line":645},[562,811,812],{"class":568},"    B->>B: settle window — confirm sole claimant\n",[562,814,815],{"class":564,"line":651},[562,816,817],{"class":568},"    B->>K: fence producer epoch (freezes the LSO)\n",[562,819,820],{"class":564,"line":657},[562,821,822],{"class":568},"    B->>K: drain changelog to LSO (only what it was missing)\n",[562,824,825],{"class":564,"line":663},[562,826,690],{"class":568},[562,828,829],{"class":564,"line":669},[562,830,831],{"class":568},"    note over B: processing resumes on caught-up state\n",[383,833,835],{"id":834},"processing-guarantees-under-failover","Processing guarantees under failover",[320,837,838,839,842],{},"How clean a failover is depends on the ",[451,840,841],{"href":53},"processing guarantee"," the application runs under.",[336,844,845,854],{},[339,846,847,850,851,853],{},[324,848,849],{},"Exactly-once (the default)."," Failover is split-brain-proof. Kafka's transactional producer fencing — a broker-enforced guarantee — ensures at most one instance can ever commit a given transaction. If a network partition or a false failure-detection briefly leaves both instances believing they are active, only one can commit; the broker fences the other, which then shuts itself down. Downstream consumers reading ",[346,852,507],{}," see exactly-once output across the handoff: no duplicates, no lost work.",[339,855,856,859,860,863],{},[324,857,858],{},"At-least-once."," Failover is ",[533,861,862],{},"bounded-duplicate",". The cluster still coordinates promotion safely — the promotion token is itself the fence under at-least-once — but without transactional fencing an ungraceful failover can re-process a small, bounded window of records, so a few duplicate output records are possible.",[465,865,868],{"color":866,"icon":867},"warning","i-lucide-shield-alert",[320,869,870],{},"For split-brain-proof failover, run hot standby under exactly-once. Under at-least-once, make downstream consumers idempotent or duplicate-tolerant and size them for a bounded-duplicate window on an ungraceful failover.",[383,872,150],{"id":873},"configuration",[320,875,876,877,880,881,884],{},"Hot standby is configured under ",[346,878,879],{},"stoatflow.ha",". The only required key is ",[346,882,883],{},"mode","; everything else has a sensible default.",[388,886,887,900],{},[391,888,889],{},[394,890,891,894,897],{},[397,892,893],{},"Key",[397,895,896],{},"Default",[397,898,899],{},"Purpose",[410,901,902,923,943,957,984,1003,1022,1040,1055,1073,1087,1122,1139],{},[394,903,904,909,914],{},[415,905,906],{},[346,907,908],{},"stoatflow.ha.mode",[415,910,911],{},[346,912,913],{},"off",[415,915,916,918,919,922],{},[346,917,913],{}," (single instance, fast restart) or ",[346,920,921],{},"active-standby"," (hot-standby cluster).",[394,924,925,930,940],{},[415,926,927],{},[346,928,929],{},"stoatflow.ha.pod-id",[415,931,932,935,936,939],{},[346,933,934],{},"POD_NAME"," env → ",[346,937,938],{},"HOSTNAME"," → local hostname",[415,941,942],{},"Stable per-pod identity that distinguishes the peers.",[394,944,945,949,954],{},[415,946,947],{},[346,948,493],{},[415,950,951],{},[346,952,953],{},"__stoatflow_\u003CapplicationId>_ha",[415,955,956],{},"Single-partition, compacted, auto-created topic the cluster coordinates through.",[394,958,959,964,969],{},[415,960,961],{},[346,962,963],{},"stoatflow.ha.desired-standbys",[415,965,966],{},[346,967,968],{},"1",[415,970,971,972,974,975,978,979,983],{},"How many caught-up standby spares the readiness gate protects. ",[346,973,968],{}," is the classic active\u002Fpassive pair; raise it (with ",[346,976,977],{},"replicas",") for more redundancy. Drives the ",[451,980,982],{"href":981},"#how-readiness-gates-a-rolling-deploy","redundancy floor"," on rolling deploys.",[394,985,986,991,996],{},[415,987,988],{},[346,989,990],{},"stoatflow.ha.max-standbys",[415,992,993],{},[346,994,995],{},"3",[415,997,998,999,1002],{},"Hard ceiling on standbys, bounding changelog fan-out (each standby is one more changelog reader). Total instances ≤ ",[346,1000,1001],{},"max-standbys + 1",".",[394,1004,1005,1010,1015],{},[415,1006,1007],{},[346,1008,1009],{},"stoatflow.ha.failover-priority",[415,1011,1012],{},[346,1013,1014],{},"0",[415,1016,1017,1018,1021],{},"Per-pod tie-break hint: orders candidates ",[533,1019,1020],{},"within"," the equal-lag bucket (higher wins). Never overrides lag or the hash floor.",[394,1023,1024,1029,1034],{},[415,1025,1026],{},[346,1027,1028],{},"stoatflow.ha.promotion-settle-window-ms",[415,1030,1031],{},[346,1032,1033],{},"500",[415,1035,1036,1037,1002],{},"After claiming the promotion token, how long the winner confirms it is the sole claimant before fencing — the single-promoter guarantee. Bounded ",[346,1038,1039],{},"1..staleness-threshold-ms",[394,1041,1042,1047,1052],{},[415,1043,1044],{},[346,1045,1046],{},"stoatflow.ha.heartbeat-ms",[415,1048,1049],{},[346,1050,1051],{},"1000",[415,1053,1054],{},"How often each pod publishes its liveness and role.",[394,1056,1057,1062,1067],{},[415,1058,1059],{},[346,1060,1061],{},"stoatflow.ha.staleness-threshold-ms",[415,1063,1064],{},[346,1065,1066],{},"5000",[415,1068,1069,1070,1002],{},"How long without a peer heartbeat before it's considered stale. Must be ≥ ",[346,1071,1072],{},"heartbeat-ms",[394,1074,1075,1080,1084],{},[415,1076,1077],{},[346,1078,1079],{},"stoatflow.ha.staleness-misses",[415,1081,1082],{},[346,1083,995],{},[415,1085,1086],{},"Consecutive missed checks before a standby is elected to promote — a debounce against transient jitter.",[394,1088,1089,1094,1099],{},[415,1090,1091],{},[346,1092,1093],{},"stoatflow.ha.acceptable-recovery-lag",[415,1095,1096],{},[346,1097,1098],{},"50000",[415,1100,1101,1102,1105,1106,1109,1110,1113,1114,1117,1118,1121],{},"Total committed-changelog lag (in records, summed across ",[324,1103,1104],{},"all"," changelog partitions) at or below which a standby is considered caught up: it reports ",[346,1107,1108],{},"READY_STANDBY"," and is eligible to promote without the grace fallback. This is a single ",[324,1111,1112],{},"total sum",", not per-task like Kafka Streams' ",[346,1115,1116],{},"acceptable.recovery.lag"," (10000) — so the KS number would be far too strict, and ~one large epoch can approach it. Correctness never depends on it — restore-before-process does; it's mostly a ",[533,1119,1120],{},"raise-me"," lever.",[394,1123,1124,1129,1133],{},[415,1125,1126],{},[346,1127,1128],{},"stoatflow.ha.promotion-grace-ms",[415,1130,1131],{},[346,1132,1066],{},[415,1134,1135,1136,1138],{},"When the active is gone and no standby is within ",[346,1137,711],{},", the least-behind standby auto-promotes after this bounded grace window (availability-first). Eligibility is re-checked continuously during the window.",[394,1140,1141,1146,1151],{},[415,1142,1143],{},[346,1144,1145],{},"stoatflow.ha.promotion-restore-no-progress-timeout-ms",[415,1147,1148],{},[346,1149,1150],{},"60000",[415,1152,1153,1154,1157,1158,1161],{},"Progress-aware deadline for restore-before-process (bringing local state to the committed changelog end before processing): fails only if a full window of this length elapses with ",[324,1155,1156],{},"zero"," records applied (genuinely stuck), ",[324,1159,1160],{},"not"," the commit-path barrier timeout. On expiry the pod self-terminates and Kubernetes restarts it (the fence already prevents split-brain).",[553,1163,1167],{"className":1164,"code":1165,"language":1166,"meta":558,"style":558},"language-yaml shiki shiki-themes vitesse-light","stoatflow:\n  application-id: my-app\n  bootstrap-servers: kafka:9092\n  ha:\n    mode: active-standby      # off (default) | active-standby\n    desired-standbys: 2       # 1 (default) = active\u002Fpassive pair; here 1 active + 2 standbys\n    max-standbys: 3           # ceiling on standbys (bounds changelog fan-out)\n    # pod-id, coordination-topic, and the timing knobs all have sensible\n    # defaults — a two-replica pair sets only mode.\n","yaml",[346,1168,1169,1179,1191,1201,1208,1222,1236,1249,1254],{"__ignoreMap":558},[562,1170,1171,1175],{"class":564,"line":565},[562,1172,1174],{"class":1173},"su6XF","stoatflow",[562,1176,1178],{"class":1177},"sYZai",":\n",[562,1180,1181,1184,1187],{"class":564,"line":572},[562,1182,1183],{"class":1173},"  application-id",[562,1185,1186],{"class":1177},":",[562,1188,1190],{"class":1189},"spphp"," my-app\n",[562,1192,1193,1196,1198],{"class":564,"line":578},[562,1194,1195],{"class":1173},"  bootstrap-servers",[562,1197,1186],{"class":1177},[562,1199,1200],{"class":1189}," kafka:9092\n",[562,1202,1203,1206],{"class":564,"line":584},[562,1204,1205],{"class":1173},"  ha",[562,1207,1178],{"class":1177},[562,1209,1210,1213,1215,1218],{"class":564,"line":590},[562,1211,1212],{"class":1173},"    mode",[562,1214,1186],{"class":1177},[562,1216,1217],{"class":1189}," active-standby",[562,1219,1221],{"class":1220},"s8zF2","      # off (default) | active-standby\n",[562,1223,1224,1227,1229,1233],{"class":564,"line":596},[562,1225,1226],{"class":1173},"    desired-standbys",[562,1228,1186],{"class":1177},[562,1230,1232],{"class":1231},"s-TwI"," 2",[562,1234,1235],{"class":1220},"       # 1 (default) = active\u002Fpassive pair; here 1 active + 2 standbys\n",[562,1237,1238,1241,1243,1246],{"class":564,"line":602},[562,1239,1240],{"class":1173},"    max-standbys",[562,1242,1186],{"class":1177},[562,1244,1245],{"class":1231}," 3",[562,1247,1248],{"class":1220},"           # ceiling on standbys (bounds changelog fan-out)\n",[562,1250,1251],{"class":564,"line":609},[562,1252,1253],{"class":1220},"    # pod-id, coordination-topic, and the timing knobs all have sensible\n",[562,1255,1256],{"class":564,"line":615},[562,1257,1258],{"class":1220},"    # defaults — a two-replica pair sets only mode.\n",[465,1260,1261],{"color":467,"icon":468},[320,1262,1263,1264,1267,1268,1270,1271,1002],{},"The staleness knobs are a downtime-versus-false-positive trade, not a correctness lever — correctness comes from the fence, so a false positive costs at most one bounded restart, never corrupt state. Tune them conservatively. On Kubernetes, ",[346,1265,1266],{},"pod-id"," resolves automatically from the downward-API ",[346,1269,934],{},", so a typical deployment sets only ",[346,1272,883],{},[1274,1275,1277],"h3",{"id":1276},"coordination-topic","Coordination topic",[320,1279,1280,1281,1283,1284,1286,1287,1289,1290,1293,1294,1297,1298,1297,1301,1304,1305,1308],{},"The cluster coordinates through one compacted internal topic (",[346,1282,493],{},", default ",[346,1285,953],{},"). It carries the per-pod heartbeats and the ",[324,1288,723],{}," (the compare-and-set that serialises the election). It is ",[324,1291,1292],{},"auto-created"," on startup; the application's Kafka principal needs ",[346,1295,1296],{},"Describe"," + ",[346,1299,1300],{},"Read",[346,1302,1303],{},"Write"," on it, plus ",[346,1306,1307],{},"Create"," if you rely on auto-creation.",[320,1310,1311],{},"Two hard requirements:",[336,1313,1314,1320],{},[339,1315,1316,1319],{},[324,1317,1318],{},"Exactly one partition."," The single-partition total order is load-bearing for the promotion-token compare-and-set — never create it with more than one partition.",[339,1321,1322,1325,1326,1329,1330,1332],{},[324,1323,1324],{},"Exclusive to one application."," The default name is ",[346,1327,1328],{},"applicationId","-scoped for exactly this reason. If you override ",[346,1331,1276],{},", give every application its own topic — sharing one across applications intermixes pod-status records and collides on the token, breaking the election and fencing.",[320,1334,1335,1336,1339,1340,1343,1344,1347],{},"Under ",[324,1337,1338],{},"at-least-once"," the topic is correctness-critical: the promotion token is itself the fence (ALO has no broker-enforced producer-epoch fence), so size its durability (replication factor, ",[346,1341,1342],{},"min.insync.replicas",") accordingly. Under ",[324,1345,1346],{},"exactly-once"," the token is an availability layer — the election — and the fence is the producer epoch.",[320,1349,1350,1351,1354,1355,1358],{},"If your cluster ",[324,1352,1353],{},"denies topic auto-creation",", pre-create the topic before starting the application. StoatFlow logs the exact required spec at ",[346,1356,1357],{},"ERROR"," and fails startup if the topic is missing and cannot be created:",[388,1360,1361,1371],{},[391,1362,1363],{},[394,1364,1365,1368],{},[397,1366,1367],{},"Property",[397,1369,1370],{},"Value",[410,1372,1373,1383,1395,1407,1422,1432,1444,1455,1467],{},[394,1374,1375,1378],{},[415,1376,1377],{},"partitions",[415,1379,1380,1382],{},[346,1381,968],{}," (required — do not increase)",[394,1384,1385,1390],{},[415,1386,1387],{},[346,1388,1389],{},"cleanup.policy",[415,1391,1392],{},[346,1393,1394],{},"compact",[394,1396,1397,1402],{},[415,1398,1399],{},[346,1400,1401],{},"retention.ms",[415,1403,1404],{},[346,1405,1406],{},"-1",[394,1408,1409,1413],{},[415,1410,1411],{},[346,1412,1342],{},[415,1414,1415,1418,1419,1421],{},[346,1416,1417],{},"2"," (",[346,1420,968],{}," on a single-broker cluster)",[394,1423,1424,1427],{},[415,1425,1426],{},"replication factor",[415,1428,1429,1431],{},[346,1430,995],{}," (or the broker count, if smaller)",[394,1433,1434,1439],{},[415,1435,1436],{},[346,1437,1438],{},"segment.ms",[415,1440,1441],{},[346,1442,1443],{},"300000",[394,1445,1446,1451],{},[415,1447,1448],{},[346,1449,1450],{},"min.compaction.lag.ms",[415,1452,1453],{},[346,1454,1014],{},[394,1456,1457,1462],{},[415,1458,1459],{},[346,1460,1461],{},"max.compaction.lag.ms",[415,1463,1464,1466],{},[346,1465,1443],{}," — forces the active segment to roll and compact, bounding the heartbeat-churn log",[394,1468,1469,1474],{},[415,1470,1471],{},[346,1472,1473],{},"min.cleanable.dirty.ratio",[415,1475,1476],{},[346,1477,1478],{},"0.1",[383,1480,1482],{"id":1481},"deploying-the-cluster-on-kubernetes","Deploying the cluster on Kubernetes",[320,1484,1485,1486,1491,1492,1495,1496,1499],{},"Run the cluster as a ",[324,1487,1488,1490],{},[346,1489,380],{}," with two or more replicas"," (not a ",[346,1493,1494],{},"Deployment","), keeping everything from the single-replica ",[451,1497,1498],{"href":229},"Kubernetes"," manifest — the state volume, license injection, config — and adjusting these points:",[336,1501,1502,1520,1539,1545,1551,1562,1571,1578],{},[339,1503,1504,1507,1508,1511,1512,1515,1516,1519],{},[346,1505,1506],{},"replicas: N"," — one active plus ",[346,1509,1510],{},"N-1"," passive standbys. ",[346,1513,1514],{},"replicas: 2"," is the classic pair (the default); the example below uses ",[346,1517,1518],{},"replicas: 3"," for one active + two standbys.",[339,1521,1522,1525,1526,1529,1530,1532,1533,1535,1536,463],{},[346,1523,1524],{},"STOATFLOW__HA__DESIRED_STANDBYS"," \u002F ",[346,1527,1528],{},"STOATFLOW__HA__MAX_STANDBYS"," — set alongside ",[346,1531,977],{}," when you run more than one standby (",[346,1534,462],{}," should be ",[346,1537,1538],{},"replicas - 1",[339,1540,1541,1544],{},[346,1542,1543],{},"updateStrategy: RollingUpdate"," — the roll is readiness-gated and order-independent (see below).",[339,1546,1547,1550],{},[346,1548,1549],{},"volumeClaimTemplates"," — each pod gets its own persistent volume, so every standby holds warm state across restarts.",[339,1552,1553,1554,1557,1558,1561],{},"Split ",[346,1555,1556],{},"\u002Fhealth\u002Flive"," and ",[346,1559,1560],{},"\u002Fhealth\u002Fready"," probes.",[339,1563,1564,1567,1568,1002],{},[346,1565,1566],{},"terminationGracePeriodSeconds"," ≥ your graceful-handoff budget (epoch drain + final commit), so a draining active finishes its handoff before ",[346,1569,1570],{},"SIGKILL",[339,1572,1573,1575,1576,1002],{},[346,1574,934],{}," from the downward API → ",[346,1577,929],{},[339,1579,1580,1581,1584,1585,463],{},"A headless ",[346,1582,1583],{},"Service"," (the StatefulSet's ",[346,1586,1587],{},"serviceName",[553,1589,1591],{"className":1164,"code":1590,"language":1166,"meta":558,"style":558},"apiVersion: apps\u002Fv1\nkind: StatefulSet\nmetadata:\n  name: my-stream-app\nspec:\n  serviceName: my-stream-app     # headless Service\n  replicas: 3                    # one active + two passive standbys (replicas: 2 = the default pair)\n  updateStrategy:\n    type: RollingUpdate          # readiness-gated; roll order doesn't matter\n  selector:\n    matchLabels:\n      app: my-stream-app\n  template:\n    metadata:\n      labels:\n        app: my-stream-app\n    spec:\n      # Must exceed the graceful-handoff budget so a draining active\n      # completes its handoff before SIGKILL.\n      terminationGracePeriodSeconds: 60\n      containers:\n        - name: app\n          image: my-registry\u002Fmy-stream-app:latest\n          ports:\n            - name: http\n              containerPort: 8080\n          env:\n            # Stable per-pod identity → stoatflow.ha.pod-id\n            - name: POD_NAME\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.name\n            - name: STOATFLOW__HA__MODE\n              value: \"active-standby\"\n            - name: STOATFLOW__HA__DESIRED_STANDBYS\n              value: \"2\"           # replicas - 1 (omit for the default pair)\n            - name: STOATFLOW__HA__MAX_STANDBYS\n              value: \"3\"\n          readinessProbe:          # gates the rolling update\n            httpGet: { path: \u002Fhealth\u002Fready, port: http }\n            periodSeconds: 5\n            failureThreshold: 3\n          livenessProbe:           # stays UP while a standby catches up\n            httpGet: { path: \u002Fhealth\u002Flive, port: http }\n            periodSeconds: 10\n            failureThreshold: 6\n          volumeMounts:\n            - name: state\n              mountPath: \u002Fvar\u002Flib\u002Fstoatflow\u002Fstate\n  volumeClaimTemplates:\n    - metadata:\n        name: state\n      spec:\n        accessModes: [\"ReadWriteOnce\"]\n        resources:\n          requests:\n            storage: 100Gi        # ≥ active state size; the standby needs the same capacity\n",[346,1592,1593,1603,1613,1620,1630,1637,1650,1662,1669,1682,1689,1696,1705,1712,1719,1726,1735,1742,1747,1752,1762,1769,1782,1793,1801,1814,1825,1833,1839,1851,1859,1867,1878,1890,1907,1919,1936,1948,1961,1972,2005,2016,2027,2038,2064,2074,2084,2092,2104,2115,2123,2134,2144,2152,2173,2181,2189],{"__ignoreMap":558},[562,1594,1595,1598,1600],{"class":564,"line":565},[562,1596,1597],{"class":1173},"apiVersion",[562,1599,1186],{"class":1177},[562,1601,1602],{"class":1189}," apps\u002Fv1\n",[562,1604,1605,1608,1610],{"class":564,"line":572},[562,1606,1607],{"class":1173},"kind",[562,1609,1186],{"class":1177},[562,1611,1612],{"class":1189}," StatefulSet\n",[562,1614,1615,1618],{"class":564,"line":578},[562,1616,1617],{"class":1173},"metadata",[562,1619,1178],{"class":1177},[562,1621,1622,1625,1627],{"class":564,"line":584},[562,1623,1624],{"class":1173},"  name",[562,1626,1186],{"class":1177},[562,1628,1629],{"class":1189}," my-stream-app\n",[562,1631,1632,1635],{"class":564,"line":590},[562,1633,1634],{"class":1173},"spec",[562,1636,1178],{"class":1177},[562,1638,1639,1642,1644,1647],{"class":564,"line":596},[562,1640,1641],{"class":1173},"  serviceName",[562,1643,1186],{"class":1177},[562,1645,1646],{"class":1189}," my-stream-app",[562,1648,1649],{"class":1220},"     # headless Service\n",[562,1651,1652,1655,1657,1659],{"class":564,"line":602},[562,1653,1654],{"class":1173},"  replicas",[562,1656,1186],{"class":1177},[562,1658,1245],{"class":1231},[562,1660,1661],{"class":1220},"                    # one active + two passive standbys (replicas: 2 = the default pair)\n",[562,1663,1664,1667],{"class":564,"line":609},[562,1665,1666],{"class":1173},"  updateStrategy",[562,1668,1178],{"class":1177},[562,1670,1671,1674,1676,1679],{"class":564,"line":615},[562,1672,1673],{"class":1173},"    type",[562,1675,1186],{"class":1177},[562,1677,1678],{"class":1189}," RollingUpdate",[562,1680,1681],{"class":1220},"          # readiness-gated; roll order doesn't matter\n",[562,1683,1684,1687],{"class":564,"line":621},[562,1685,1686],{"class":1173},"  selector",[562,1688,1178],{"class":1177},[562,1690,1691,1694],{"class":564,"line":627},[562,1692,1693],{"class":1173},"    matchLabels",[562,1695,1178],{"class":1177},[562,1697,1698,1701,1703],{"class":564,"line":633},[562,1699,1700],{"class":1173},"      app",[562,1702,1186],{"class":1177},[562,1704,1629],{"class":1189},[562,1706,1707,1710],{"class":564,"line":639},[562,1708,1709],{"class":1173},"  template",[562,1711,1178],{"class":1177},[562,1713,1714,1717],{"class":564,"line":645},[562,1715,1716],{"class":1173},"    metadata",[562,1718,1178],{"class":1177},[562,1720,1721,1724],{"class":564,"line":651},[562,1722,1723],{"class":1173},"      labels",[562,1725,1178],{"class":1177},[562,1727,1728,1731,1733],{"class":564,"line":657},[562,1729,1730],{"class":1173},"        app",[562,1732,1186],{"class":1177},[562,1734,1629],{"class":1189},[562,1736,1737,1740],{"class":564,"line":663},[562,1738,1739],{"class":1173},"    spec",[562,1741,1178],{"class":1177},[562,1743,1744],{"class":564,"line":669},[562,1745,1746],{"class":1220},"      # Must exceed the graceful-handoff budget so a draining active\n",[562,1748,1749],{"class":564,"line":675},[562,1750,1751],{"class":1220},"      # completes its handoff before SIGKILL.\n",[562,1753,1754,1757,1759],{"class":564,"line":681},[562,1755,1756],{"class":1173},"      terminationGracePeriodSeconds",[562,1758,1186],{"class":1177},[562,1760,1761],{"class":1231}," 60\n",[562,1763,1764,1767],{"class":564,"line":687},[562,1765,1766],{"class":1173},"      containers",[562,1768,1178],{"class":1177},[562,1770,1771,1774,1777,1779],{"class":564,"line":693},[562,1772,1773],{"class":1177},"        -",[562,1775,1776],{"class":1173}," name",[562,1778,1186],{"class":1177},[562,1780,1781],{"class":1189}," app\n",[562,1783,1785,1788,1790],{"class":564,"line":1784},23,[562,1786,1787],{"class":1173},"          image",[562,1789,1186],{"class":1177},[562,1791,1792],{"class":1189}," my-registry\u002Fmy-stream-app:latest\n",[562,1794,1796,1799],{"class":564,"line":1795},24,[562,1797,1798],{"class":1173},"          ports",[562,1800,1178],{"class":1177},[562,1802,1804,1807,1809,1811],{"class":564,"line":1803},25,[562,1805,1806],{"class":1177},"            -",[562,1808,1776],{"class":1173},[562,1810,1186],{"class":1177},[562,1812,1813],{"class":1189}," http\n",[562,1815,1817,1820,1822],{"class":564,"line":1816},26,[562,1818,1819],{"class":1173},"              containerPort",[562,1821,1186],{"class":1177},[562,1823,1824],{"class":1231}," 8080\n",[562,1826,1828,1831],{"class":564,"line":1827},27,[562,1829,1830],{"class":1173},"          env",[562,1832,1178],{"class":1177},[562,1834,1836],{"class":564,"line":1835},28,[562,1837,1838],{"class":1220},"            # Stable per-pod identity → stoatflow.ha.pod-id\n",[562,1840,1842,1844,1846,1848],{"class":564,"line":1841},29,[562,1843,1806],{"class":1177},[562,1845,1776],{"class":1173},[562,1847,1186],{"class":1177},[562,1849,1850],{"class":1189}," POD_NAME\n",[562,1852,1854,1857],{"class":564,"line":1853},30,[562,1855,1856],{"class":1173},"              valueFrom",[562,1858,1178],{"class":1177},[562,1860,1862,1865],{"class":564,"line":1861},31,[562,1863,1864],{"class":1173},"                fieldRef",[562,1866,1178],{"class":1177},[562,1868,1870,1873,1875],{"class":564,"line":1869},32,[562,1871,1872],{"class":1173},"                  fieldPath",[562,1874,1186],{"class":1177},[562,1876,1877],{"class":1189}," metadata.name\n",[562,1879,1881,1883,1885,1887],{"class":564,"line":1880},33,[562,1882,1806],{"class":1177},[562,1884,1776],{"class":1173},[562,1886,1186],{"class":1177},[562,1888,1889],{"class":1189}," STOATFLOW__HA__MODE\n",[562,1891,1893,1896,1898,1902,1904],{"class":564,"line":1892},34,[562,1894,1895],{"class":1173},"              value",[562,1897,1186],{"class":1177},[562,1899,1901],{"class":1900},"sSP4y"," \"",[562,1903,921],{"class":1189},[562,1905,1906],{"class":1900},"\"\n",[562,1908,1910,1912,1914,1916],{"class":564,"line":1909},35,[562,1911,1806],{"class":1177},[562,1913,1776],{"class":1173},[562,1915,1186],{"class":1177},[562,1917,1918],{"class":1189}," STOATFLOW__HA__DESIRED_STANDBYS\n",[562,1920,1922,1924,1926,1928,1930,1933],{"class":564,"line":1921},36,[562,1923,1895],{"class":1173},[562,1925,1186],{"class":1177},[562,1927,1901],{"class":1900},[562,1929,1417],{"class":1189},[562,1931,1932],{"class":1900},"\"",[562,1934,1935],{"class":1220},"           # replicas - 1 (omit for the default pair)\n",[562,1937,1939,1941,1943,1945],{"class":564,"line":1938},37,[562,1940,1806],{"class":1177},[562,1942,1776],{"class":1173},[562,1944,1186],{"class":1177},[562,1946,1947],{"class":1189}," STOATFLOW__HA__MAX_STANDBYS\n",[562,1949,1951,1953,1955,1957,1959],{"class":564,"line":1950},38,[562,1952,1895],{"class":1173},[562,1954,1186],{"class":1177},[562,1956,1901],{"class":1900},[562,1958,995],{"class":1189},[562,1960,1906],{"class":1900},[562,1962,1964,1967,1969],{"class":564,"line":1963},39,[562,1965,1966],{"class":1173},"          readinessProbe",[562,1968,1186],{"class":1177},[562,1970,1971],{"class":1220},"          # gates the rolling update\n",[562,1973,1975,1978,1980,1983,1986,1988,1991,1994,1997,1999,2002],{"class":564,"line":1974},40,[562,1976,1977],{"class":1173},"            httpGet",[562,1979,1186],{"class":1177},[562,1981,1982],{"class":1177}," {",[562,1984,1985],{"class":1173}," path",[562,1987,1186],{"class":1177},[562,1989,1990],{"class":1189}," \u002Fhealth\u002Fready",[562,1992,1993],{"class":1177},",",[562,1995,1996],{"class":1173}," port",[562,1998,1186],{"class":1177},[562,2000,2001],{"class":1189}," http",[562,2003,2004],{"class":1177}," }\n",[562,2006,2008,2011,2013],{"class":564,"line":2007},41,[562,2009,2010],{"class":1173},"            periodSeconds",[562,2012,1186],{"class":1177},[562,2014,2015],{"class":1231}," 5\n",[562,2017,2019,2022,2024],{"class":564,"line":2018},42,[562,2020,2021],{"class":1173},"            failureThreshold",[562,2023,1186],{"class":1177},[562,2025,2026],{"class":1231}," 3\n",[562,2028,2030,2033,2035],{"class":564,"line":2029},43,[562,2031,2032],{"class":1173},"          livenessProbe",[562,2034,1186],{"class":1177},[562,2036,2037],{"class":1220},"           # stays UP while a standby catches up\n",[562,2039,2041,2043,2045,2047,2049,2051,2054,2056,2058,2060,2062],{"class":564,"line":2040},44,[562,2042,1977],{"class":1173},[562,2044,1186],{"class":1177},[562,2046,1982],{"class":1177},[562,2048,1985],{"class":1173},[562,2050,1186],{"class":1177},[562,2052,2053],{"class":1189}," \u002Fhealth\u002Flive",[562,2055,1993],{"class":1177},[562,2057,1996],{"class":1173},[562,2059,1186],{"class":1177},[562,2061,2001],{"class":1189},[562,2063,2004],{"class":1177},[562,2065,2067,2069,2071],{"class":564,"line":2066},45,[562,2068,2010],{"class":1173},[562,2070,1186],{"class":1177},[562,2072,2073],{"class":1231}," 10\n",[562,2075,2077,2079,2081],{"class":564,"line":2076},46,[562,2078,2021],{"class":1173},[562,2080,1186],{"class":1177},[562,2082,2083],{"class":1231}," 6\n",[562,2085,2087,2090],{"class":564,"line":2086},47,[562,2088,2089],{"class":1173},"          volumeMounts",[562,2091,1178],{"class":1177},[562,2093,2095,2097,2099,2101],{"class":564,"line":2094},48,[562,2096,1806],{"class":1177},[562,2098,1776],{"class":1173},[562,2100,1186],{"class":1177},[562,2102,2103],{"class":1189}," state\n",[562,2105,2107,2110,2112],{"class":564,"line":2106},49,[562,2108,2109],{"class":1173},"              mountPath",[562,2111,1186],{"class":1177},[562,2113,2114],{"class":1189}," \u002Fvar\u002Flib\u002Fstoatflow\u002Fstate\n",[562,2116,2118,2121],{"class":564,"line":2117},50,[562,2119,2120],{"class":1173},"  volumeClaimTemplates",[562,2122,1178],{"class":1177},[562,2124,2126,2129,2132],{"class":564,"line":2125},51,[562,2127,2128],{"class":1177},"    -",[562,2130,2131],{"class":1173}," metadata",[562,2133,1178],{"class":1177},[562,2135,2137,2140,2142],{"class":564,"line":2136},52,[562,2138,2139],{"class":1173},"        name",[562,2141,1186],{"class":1177},[562,2143,2103],{"class":1189},[562,2145,2147,2150],{"class":564,"line":2146},53,[562,2148,2149],{"class":1173},"      spec",[562,2151,1178],{"class":1177},[562,2153,2155,2158,2160,2163,2165,2168,2170],{"class":564,"line":2154},54,[562,2156,2157],{"class":1173},"        accessModes",[562,2159,1186],{"class":1177},[562,2161,2162],{"class":1177}," [",[562,2164,1932],{"class":1900},[562,2166,2167],{"class":1189},"ReadWriteOnce",[562,2169,1932],{"class":1900},[562,2171,2172],{"class":1177},"]\n",[562,2174,2176,2179],{"class":564,"line":2175},55,[562,2177,2178],{"class":1173},"        resources",[562,2180,1178],{"class":1177},[562,2182,2184,2187],{"class":564,"line":2183},56,[562,2185,2186],{"class":1173},"          requests",[562,2188,1178],{"class":1177},[562,2190,2192,2195,2197,2200],{"class":564,"line":2191},57,[562,2193,2194],{"class":1173},"            storage",[562,2196,1186],{"class":1177},[562,2198,2199],{"class":1189}," 100Gi",[562,2201,2202],{"class":1220},"        # ≥ active state size; the standby needs the same capacity\n",[465,2204,2205],{"color":866,"icon":80},[320,2206,2207,2209,2210,355,2213,2215,2216,2218,2219,2223],{},[346,2208,1514],{}," or more is valid ",[324,2211,2212],{},"only",[346,2214,481],{},". Multiple instances of the same application with ",[346,2217,358],{}," will corrupt state — see ",[451,2220,2222],{"href":2221},"\u002Fdocs\u002Foperating#one-active-instance","One active instance",". The standbys are passive precisely so the cluster can run more than one replica safely.",[383,2225,2227],{"id":2226},"how-readiness-gates-a-rolling-deploy","How readiness gates a rolling deploy",[320,2229,2230],{},"The split health probes are what make the rolling update safe and order-independent.",[388,2232,2233,2248],{},[391,2234,2235],{},[394,2236,2237,2240,2244],{},[397,2238,2239],{},"Role",[397,2241,2242],{},[346,2243,1560],{},[397,2245,2246],{},[346,2247,1556],{},[410,2249,2250,2263],{},[394,2251,2252,2257,2260],{},[415,2253,2254],{},[324,2255,2256],{},"Active",[415,2258,2259],{},"UP while it is processing",[415,2261,2262],{},"UP while running",[394,2264,2265,2270,2273],{},[415,2266,2267],{},[324,2268,2269],{},"Standby",[415,2271,2272],{},"UP only once it has caught up",[415,2274,2275],{},"UP while it is still catching up (not killed)",[320,2277,2278,2279,1557,2282,1002],{},"Because the standby reports ready only after it has caught up, Kubernetes won't move on to the next pod in the roll until the one it just restarted is a caught-up standby. And because a still-catching-up standby keeps its liveness probe UP, Kubernetes won't kill it mid-catch-up. The result is a safe rolling deploy with no custom controller — whichever pod rolls first, the cluster never cuts over to an instance that isn't ready. See ",[451,2280,2281],{"href":239},"Probes",[451,2283,190],{"href":191},[320,2285,2286,2289,2290,2292],{},[324,2287,2288],{},"The redundancy floor."," With more than one standby, readiness is gated on redundancy as well as catch-up: a caught-up standby reports ready to roll only if rolling it would still leave the active plus at least one other caught-up standby (governed by ",[346,2291,462],{},"). So a rolling deploy takes one pod down at a time, standbys first and the active last, and never drops below an active and a warm spare while it runs. A two-replica pair has no other spare to preserve, so this reduces to the pair's behaviour.",[383,2294,2296],{"id":2295},"operating-the-cluster","Operating the cluster",[320,2298,2299,2300,1002],{},"When HA is enabled, each pod exposes a small set of HTTP endpoints (in-cluster). They are not registered when ",[346,2301,2302],{},"ha.mode: off",[388,2304,2305,2317],{},[391,2306,2307],{},[394,2308,2309,2312,2315],{},[397,2310,2311],{},"Endpoint",[397,2313,2314],{},"Method",[397,2316,899],{},[410,2318,2319,2339,2362,2379],{},[394,2320,2321,2326,2329],{},[415,2322,2323],{},[346,2324,2325],{},"\u002Fha\u002Fstatus",[415,2327,2328],{},"GET",[415,2330,2331,2332,1525,2335,2338],{},"This pod's view of the cluster: its own role, total committed-changelog lag, the freshness of its coordination view (",[346,2333,2334],{},"tailFreshnessMs",[346,2336,2337],{},"tailCaughtUp","), the caught-up-standby count, the current promotion-token epoch\u002Fholder, and the peers it observes.",[394,2340,2341,2346,2349],{},[415,2342,2343],{},[346,2344,2345],{},"\u002Fha\u002Fswitch",[415,2347,2348],{},"POST",[415,2350,2351,2352,2355,2356,2358,2359,1002],{},"Swap roles — whichever pod is active drains and the peer promotes. Rejected ",[346,2353,2354],{},"409"," unless a caught-up (",[346,2357,1108],{},") peer exists, or ",[346,2360,2361],{},"?force=true",[394,2363,2364,2369,2371],{},[415,2365,2366],{},[346,2367,2368],{},"\u002Fha\u002Fpromote?pod=\u003Cid>",[415,2370,2348],{},[415,2372,2373,2374,2376,2377,1002],{},"Ask a specific standby pod to promote. Rejected ",[346,2375,2354],{}," unless the target is caught up, or ",[346,2378,2361],{},[394,2380,2381,2386,2388],{},[415,2382,2383],{},[346,2384,2385],{},"\u002Fha\u002Fdemote?pod=\u003Cid>",[415,2387,2348],{},[415,2389,2390,2391,2393,2394,1002],{},"Ask a specific active pod to demote. Rejected ",[346,2392,2354],{}," unless a caught-up peer can take over, or ",[346,2395,2361],{},[553,2397,2401],{"className":2398,"code":2399,"language":2400,"meta":558,"style":558},"language-bash shiki shiki-themes vitesse-light","curl -s localhost:8080\u002Fha\u002Fstatus\n","bash",[346,2402,2403],{"__ignoreMap":558},[562,2404,2405,2409,2413],{"class":564,"line":565},[562,2406,2408],{"class":2407},"sySUi","curl",[562,2410,2412],{"class":2411},"sEi1f"," -s",[562,2414,2415],{"class":1189}," localhost:8080\u002Fha\u002Fstatus\n",[553,2417,2421],{"className":2418,"code":2419,"language":2420,"meta":558,"style":558},"language-json shiki shiki-themes vitesse-light","{\n  \"selfPodId\": \"my-stream-app-0\",\n  \"selfRole\": \"ACTIVE\",\n  \"selfState\": \"ACTIVE\",\n  \"replicationLagRecords\": 0,\n  \"replicationLagMs\": 0,\n  \"tailFreshnessMs\": 180,\n  \"tailCaughtUp\": true,\n  \"restartRequired\": null,\n  \"readyStandbyCount\": 2,\n  \"desiredStandbys\": 2,\n  \"redundancyBelowDesired\": false,\n  \"tokenEpoch\": 1,\n  \"tokenHolder\": \"my-stream-app-0\",\n  \"peers\": [\n    {\n      \"podId\": \"my-stream-app-1\",\n      \"role\": \"STANDBY\",\n      \"state\": \"READY_STANDBY\",\n      \"replicationLagRecords\": 1240,\n      \"replicationLagMs\": 85,\n      \"failoverPriority\": 0,\n      \"generation\": 7,\n      \"freshnessMs\": 420\n    },\n    {\n      \"podId\": \"my-stream-app-2\",\n      \"role\": \"STANDBY\",\n      \"state\": \"READY_STANDBY\",\n      \"replicationLagRecords\": 980,\n      \"replicationLagMs\": 70,\n      \"failoverPriority\": 0,\n      \"generation\": 7,\n      \"freshnessMs\": 310\n    }\n  ]\n}\n","json",[346,2422,2423,2428,2451,2471,2490,2506,2521,2536,2552,2568,2583,2598,2614,2630,2649,2663,2668,2689,2709,2728,2743,2758,2773,2789,2803,2808,2812,2831,2849,2867,2882,2897,2911,2925,2938,2943,2948],{"__ignoreMap":558},[562,2424,2425],{"class":564,"line":565},[562,2426,2427],{"class":1177},"{\n",[562,2429,2430,2434,2437,2439,2441,2443,2446,2448],{"class":564,"line":572},[562,2431,2433],{"class":2432},"s61at","  \"",[562,2435,2436],{"class":1173},"selfPodId",[562,2438,1932],{"class":2432},[562,2440,1186],{"class":1177},[562,2442,1901],{"class":1900},[562,2444,2445],{"class":1189},"my-stream-app-0",[562,2447,1932],{"class":1900},[562,2449,2450],{"class":1177},",\n",[562,2452,2453,2455,2458,2460,2462,2464,2467,2469],{"class":564,"line":578},[562,2454,2433],{"class":2432},[562,2456,2457],{"class":1173},"selfRole",[562,2459,1932],{"class":2432},[562,2461,1186],{"class":1177},[562,2463,1901],{"class":1900},[562,2465,2466],{"class":1189},"ACTIVE",[562,2468,1932],{"class":1900},[562,2470,2450],{"class":1177},[562,2472,2473,2475,2478,2480,2482,2484,2486,2488],{"class":564,"line":584},[562,2474,2433],{"class":2432},[562,2476,2477],{"class":1173},"selfState",[562,2479,1932],{"class":2432},[562,2481,1186],{"class":1177},[562,2483,1901],{"class":1900},[562,2485,2466],{"class":1189},[562,2487,1932],{"class":1900},[562,2489,2450],{"class":1177},[562,2491,2492,2494,2497,2499,2501,2504],{"class":564,"line":590},[562,2493,2433],{"class":2432},[562,2495,2496],{"class":1173},"replicationLagRecords",[562,2498,1932],{"class":2432},[562,2500,1186],{"class":1177},[562,2502,2503],{"class":1231}," 0",[562,2505,2450],{"class":1177},[562,2507,2508,2510,2513,2515,2517,2519],{"class":564,"line":596},[562,2509,2433],{"class":2432},[562,2511,2512],{"class":1173},"replicationLagMs",[562,2514,1932],{"class":2432},[562,2516,1186],{"class":1177},[562,2518,2503],{"class":1231},[562,2520,2450],{"class":1177},[562,2522,2523,2525,2527,2529,2531,2534],{"class":564,"line":602},[562,2524,2433],{"class":2432},[562,2526,2334],{"class":1173},[562,2528,1932],{"class":2432},[562,2530,1186],{"class":1177},[562,2532,2533],{"class":1231}," 180",[562,2535,2450],{"class":1177},[562,2537,2538,2540,2542,2544,2546,2550],{"class":564,"line":609},[562,2539,2433],{"class":2432},[562,2541,2337],{"class":1173},[562,2543,1932],{"class":2432},[562,2545,1186],{"class":1177},[562,2547,2549],{"class":2548},"sbBg2"," true",[562,2551,2450],{"class":1177},[562,2553,2554,2556,2559,2561,2563,2566],{"class":564,"line":615},[562,2555,2433],{"class":2432},[562,2557,2558],{"class":1173},"restartRequired",[562,2560,1932],{"class":2432},[562,2562,1186],{"class":1177},[562,2564,2565],{"class":2548}," null",[562,2567,2450],{"class":1177},[562,2569,2570,2572,2575,2577,2579,2581],{"class":564,"line":621},[562,2571,2433],{"class":2432},[562,2573,2574],{"class":1173},"readyStandbyCount",[562,2576,1932],{"class":2432},[562,2578,1186],{"class":1177},[562,2580,1232],{"class":1231},[562,2582,2450],{"class":1177},[562,2584,2585,2587,2590,2592,2594,2596],{"class":564,"line":627},[562,2586,2433],{"class":2432},[562,2588,2589],{"class":1173},"desiredStandbys",[562,2591,1932],{"class":2432},[562,2593,1186],{"class":1177},[562,2595,1232],{"class":1231},[562,2597,2450],{"class":1177},[562,2599,2600,2602,2605,2607,2609,2612],{"class":564,"line":633},[562,2601,2433],{"class":2432},[562,2603,2604],{"class":1173},"redundancyBelowDesired",[562,2606,1932],{"class":2432},[562,2608,1186],{"class":1177},[562,2610,2611],{"class":2548}," false",[562,2613,2450],{"class":1177},[562,2615,2616,2618,2621,2623,2625,2628],{"class":564,"line":639},[562,2617,2433],{"class":2432},[562,2619,2620],{"class":1173},"tokenEpoch",[562,2622,1932],{"class":2432},[562,2624,1186],{"class":1177},[562,2626,2627],{"class":1231}," 1",[562,2629,2450],{"class":1177},[562,2631,2632,2634,2637,2639,2641,2643,2645,2647],{"class":564,"line":645},[562,2633,2433],{"class":2432},[562,2635,2636],{"class":1173},"tokenHolder",[562,2638,1932],{"class":2432},[562,2640,1186],{"class":1177},[562,2642,1901],{"class":1900},[562,2644,2445],{"class":1189},[562,2646,1932],{"class":1900},[562,2648,2450],{"class":1177},[562,2650,2651,2653,2656,2658,2660],{"class":564,"line":651},[562,2652,2433],{"class":2432},[562,2654,2655],{"class":1173},"peers",[562,2657,1932],{"class":2432},[562,2659,1186],{"class":1177},[562,2661,2662],{"class":1177}," [\n",[562,2664,2665],{"class":564,"line":657},[562,2666,2667],{"class":1177},"    {\n",[562,2669,2670,2673,2676,2678,2680,2682,2685,2687],{"class":564,"line":663},[562,2671,2672],{"class":2432},"      \"",[562,2674,2675],{"class":1173},"podId",[562,2677,1932],{"class":2432},[562,2679,1186],{"class":1177},[562,2681,1901],{"class":1900},[562,2683,2684],{"class":1189},"my-stream-app-1",[562,2686,1932],{"class":1900},[562,2688,2450],{"class":1177},[562,2690,2691,2693,2696,2698,2700,2702,2705,2707],{"class":564,"line":669},[562,2692,2672],{"class":2432},[562,2694,2695],{"class":1173},"role",[562,2697,1932],{"class":2432},[562,2699,1186],{"class":1177},[562,2701,1901],{"class":1900},[562,2703,2704],{"class":1189},"STANDBY",[562,2706,1932],{"class":1900},[562,2708,2450],{"class":1177},[562,2710,2711,2713,2716,2718,2720,2722,2724,2726],{"class":564,"line":675},[562,2712,2672],{"class":2432},[562,2714,2715],{"class":1173},"state",[562,2717,1932],{"class":2432},[562,2719,1186],{"class":1177},[562,2721,1901],{"class":1900},[562,2723,1108],{"class":1189},[562,2725,1932],{"class":1900},[562,2727,2450],{"class":1177},[562,2729,2730,2732,2734,2736,2738,2741],{"class":564,"line":681},[562,2731,2672],{"class":2432},[562,2733,2496],{"class":1173},[562,2735,1932],{"class":2432},[562,2737,1186],{"class":1177},[562,2739,2740],{"class":1231}," 1240",[562,2742,2450],{"class":1177},[562,2744,2745,2747,2749,2751,2753,2756],{"class":564,"line":687},[562,2746,2672],{"class":2432},[562,2748,2512],{"class":1173},[562,2750,1932],{"class":2432},[562,2752,1186],{"class":1177},[562,2754,2755],{"class":1231}," 85",[562,2757,2450],{"class":1177},[562,2759,2760,2762,2765,2767,2769,2771],{"class":564,"line":693},[562,2761,2672],{"class":2432},[562,2763,2764],{"class":1173},"failoverPriority",[562,2766,1932],{"class":2432},[562,2768,1186],{"class":1177},[562,2770,2503],{"class":1231},[562,2772,2450],{"class":1177},[562,2774,2775,2777,2780,2782,2784,2787],{"class":564,"line":1784},[562,2776,2672],{"class":2432},[562,2778,2779],{"class":1173},"generation",[562,2781,1932],{"class":2432},[562,2783,1186],{"class":1177},[562,2785,2786],{"class":1231}," 7",[562,2788,2450],{"class":1177},[562,2790,2791,2793,2796,2798,2800],{"class":564,"line":1795},[562,2792,2672],{"class":2432},[562,2794,2795],{"class":1173},"freshnessMs",[562,2797,1932],{"class":2432},[562,2799,1186],{"class":1177},[562,2801,2802],{"class":1231}," 420\n",[562,2804,2805],{"class":564,"line":1803},[562,2806,2807],{"class":1177},"    },\n",[562,2809,2810],{"class":564,"line":1816},[562,2811,2667],{"class":1177},[562,2813,2814,2816,2818,2820,2822,2824,2827,2829],{"class":564,"line":1827},[562,2815,2672],{"class":2432},[562,2817,2675],{"class":1173},[562,2819,1932],{"class":2432},[562,2821,1186],{"class":1177},[562,2823,1901],{"class":1900},[562,2825,2826],{"class":1189},"my-stream-app-2",[562,2828,1932],{"class":1900},[562,2830,2450],{"class":1177},[562,2832,2833,2835,2837,2839,2841,2843,2845,2847],{"class":564,"line":1835},[562,2834,2672],{"class":2432},[562,2836,2695],{"class":1173},[562,2838,1932],{"class":2432},[562,2840,1186],{"class":1177},[562,2842,1901],{"class":1900},[562,2844,2704],{"class":1189},[562,2846,1932],{"class":1900},[562,2848,2450],{"class":1177},[562,2850,2851,2853,2855,2857,2859,2861,2863,2865],{"class":564,"line":1841},[562,2852,2672],{"class":2432},[562,2854,2715],{"class":1173},[562,2856,1932],{"class":2432},[562,2858,1186],{"class":1177},[562,2860,1901],{"class":1900},[562,2862,1108],{"class":1189},[562,2864,1932],{"class":1900},[562,2866,2450],{"class":1177},[562,2868,2869,2871,2873,2875,2877,2880],{"class":564,"line":1853},[562,2870,2672],{"class":2432},[562,2872,2496],{"class":1173},[562,2874,1932],{"class":2432},[562,2876,1186],{"class":1177},[562,2878,2879],{"class":1231}," 980",[562,2881,2450],{"class":1177},[562,2883,2884,2886,2888,2890,2892,2895],{"class":564,"line":1861},[562,2885,2672],{"class":2432},[562,2887,2512],{"class":1173},[562,2889,1932],{"class":2432},[562,2891,1186],{"class":1177},[562,2893,2894],{"class":1231}," 70",[562,2896,2450],{"class":1177},[562,2898,2899,2901,2903,2905,2907,2909],{"class":564,"line":1869},[562,2900,2672],{"class":2432},[562,2902,2764],{"class":1173},[562,2904,1932],{"class":2432},[562,2906,1186],{"class":1177},[562,2908,2503],{"class":1231},[562,2910,2450],{"class":1177},[562,2912,2913,2915,2917,2919,2921,2923],{"class":564,"line":1880},[562,2914,2672],{"class":2432},[562,2916,2779],{"class":1173},[562,2918,1932],{"class":2432},[562,2920,1186],{"class":1177},[562,2922,2786],{"class":1231},[562,2924,2450],{"class":1177},[562,2926,2927,2929,2931,2933,2935],{"class":564,"line":1892},[562,2928,2672],{"class":2432},[562,2930,2795],{"class":1173},[562,2932,1932],{"class":2432},[562,2934,1186],{"class":1177},[562,2936,2937],{"class":1231}," 310\n",[562,2939,2940],{"class":564,"line":1909},[562,2941,2942],{"class":1177},"    }\n",[562,2944,2945],{"class":564,"line":1921},[562,2946,2947],{"class":1177},"  ]\n",[562,2949,2950],{"class":564,"line":1938},[562,2951,2952],{"class":1177},"}\n",[320,2954,2955,2956,2959,2960,1557,2963,2966,2967,2970,2971,2974],{},"The write endpoints are thin shims: they publish a command and return ",[346,2957,2958],{},"202 Accepted"," with the command's offset as an idempotency token; the targeted pod observes it and acts. ",[346,2961,2962],{},"promote",[346,2964,2965],{},"demote"," require a ",[346,2968,2969],{},"?pod=\u003Cid>"," target; ",[346,2972,2973],{},"switch"," targets whichever pod is currently active.",[320,2976,2977,2978,2980,2981,2980,2983,2985,2986,2355,2992,2994,2995,2999,3000,3003,3004,3007],{},"To prevent a self-inflicted outage, ",[346,2979,2973],{},"\u002F",[346,2982,2962],{},[346,2984,2965],{}," are ",[324,2987,2988,2989],{},"rejected with ",[346,2990,2991],{},"409 Conflict",[346,2993,1108],{},") target exists. Override with ",[324,2996,2997],{},[346,2998,2361],{}," when you deliberately want to hand off to a behind pod — a forced promotion still fully restores to the committed changelog before processing (correctness is never traded; ",[346,3001,3002],{},"force"," only skips the ",[533,3005,3006],{},"readiness"," check).",[553,3009,3011],{"className":2398,"code":3010,"language":2400,"meta":558,"style":558},"curl -X POST localhost:8080\u002Fha\u002Fswitch\n# { \"command\": \"SWITCH\", \"target\": null, \"force\": false, \"commandOffset\": 42 }\n\ncurl -X POST 'localhost:8080\u002Fha\u002Fpromote?pod=my-stream-app-1&force=true'\n# { \"command\": \"PROMOTE\", \"target\": \"my-stream-app-1\", \"force\": true, \"commandOffset\": 43 }\n",[346,3012,3013,3026,3031,3035,3052],{"__ignoreMap":558},[562,3014,3015,3017,3020,3023],{"class":564,"line":565},[562,3016,2408],{"class":2407},[562,3018,3019],{"class":2411}," -X",[562,3021,3022],{"class":1189}," POST",[562,3024,3025],{"class":1189}," localhost:8080\u002Fha\u002Fswitch\n",[562,3027,3028],{"class":564,"line":572},[562,3029,3030],{"class":1220},"# { \"command\": \"SWITCH\", \"target\": null, \"force\": false, \"commandOffset\": 42 }\n",[562,3032,3033],{"class":564,"line":578},[562,3034,606],{"emptyLinePlaceholder":605},[562,3036,3037,3039,3041,3043,3046,3049],{"class":564,"line":584},[562,3038,2408],{"class":2407},[562,3040,3019],{"class":2411},[562,3042,3022],{"class":1189},[562,3044,3045],{"class":1900}," '",[562,3047,3048],{"class":1189},"localhost:8080\u002Fha\u002Fpromote?pod=my-stream-app-1&force=true",[562,3050,3051],{"class":1900},"'\n",[562,3053,3054],{"class":564,"line":590},[562,3055,3056],{"class":1220},"# { \"command\": \"PROMOTE\", \"target\": \"my-stream-app-1\", \"force\": true, \"commandOffset\": 43 }\n",[320,3058,3059,1525,3061,3063,3064,3067],{},[346,3060,2334],{},[346,3062,2337],{}," report how current this pod's view of the coordination topic is. A pod whose tail has gone stale (",[346,3065,3066],{},"tailCaughtUp: false",") defers failover decisions until its view is current again — correctness is unaffected (the fence is the split-brain backstop), but a persistently stale tail on a pod is worth investigating.",[320,3069,3070,3071,3074],{},"Every role change is recorded on the coordination topic, so its full history can be replayed with a console consumer if you need an audit trail. See ",[451,3072,3073],{"href":186},"REST API"," for the complete endpoint reference.",[383,3076,3078],{"id":3077},"metrics-and-what-to-alert-on","Metrics and what to alert on",[320,3080,3081,3082,3085],{},"The runtime exposes HA metrics on ",[346,3083,3084],{},"\u002Fmetrics"," (Prometheus) when HA is enabled.",[388,3087,3088,3101],{},[391,3089,3090],{},[394,3091,3092,3095,3098],{},[397,3093,3094],{},"Metric",[397,3096,3097],{},"Meaning",[397,3099,3100],{},"Alert on",[410,3102,3103,3121,3143,3160,3173,3189,3205,3223,3243,3256,3277,3293],{},[394,3104,3105,3110,3118],{},[415,3106,3107],{},[346,3108,3109],{},"stoatflow.ha.role",[415,3111,3112,3114,3115,3117],{},[346,3113,968],{}," when this pod is the active, ",[346,3116,1014],{}," when standby.",[415,3119,3120],{},"— (use for dashboards \u002F \"is there exactly one active?\").",[394,3122,3123,3128,3138],{},[415,3124,3125],{},[346,3126,3127],{},"stoatflow.ha.standby.replication.lag.records",[415,3129,3130,3131,3134,3135,3137],{},"Gauge — true ",[324,3132,3133],{},"total"," committed-changelog lag in records, summed across all changelog partitions and read directly from the standby applier consumer's position; no admin round-trip (",[346,3136,1014],{}," when active or caught up).",[415,3139,3140,3141,1002],{},"Sustained growth, or staying above ",[346,3142,711],{},[394,3144,3145,3150,3157],{},[415,3146,3147],{},[346,3148,3149],{},"stoatflow.ha.standby.replication.applied.records",[415,3151,3152,3153,3156],{},"Counter — cumulative committed changelog deltas the streaming standby applier has applied; ",[346,3154,3155],{},"rate(...)"," gives apply throughput (resets on promotion\u002Frestart).",[415,3158,3159],{},"— (dashboards).",[394,3161,3162,3167,3170],{},[415,3163,3164],{},[346,3165,3166],{},"stoatflow.ha.standby.replication.lag.ms",[415,3168,3169],{},"Gauge — standby replication lag in milliseconds.",[415,3171,3172],{},"Sustained values above your replication-lag SLO.",[394,3174,3175,3180,3183],{},[415,3176,3177],{},[346,3178,3179],{},"stoatflow.ha.peer.freshness.ms",[415,3181,3182],{},"Time since the freshest peer's last heartbeat.",[415,3184,3185,3186,1002],{},"Approaches ",[346,3187,3188],{},"staleness-threshold-ms",[394,3190,3191,3196,3199],{},[415,3192,3193],{},[346,3194,3195],{},"stoatflow.ha.peers",[415,3197,3198],{},"Number of observed peers.",[415,3200,3201,3202,3204],{},"Drops below ",[346,3203,462],{}," (lost redundancy).",[394,3206,3207,3212,3218],{},[415,3208,3209],{},[346,3210,3211],{},"stoatflow.ha.redundancy.ready.standbys",[415,3213,3214,3215,3217],{},"Gauge — count of caught-up (",[346,3216,1108],{},") standbys, including self.",[415,3219,3220,3221,1002],{},"Below ",[346,3222,462],{},[394,3224,3225,3230,3238],{},[415,3226,3227],{},[346,3228,3229],{},"stoatflow.ha.redundancy.below.desired",[415,3231,3232,3234,3235,3237],{},[346,3233,968],{}," when the active has fewer ready standby spares than ",[346,3236,462],{}," — a single point of failure.",[415,3239,3240,3241,1002],{},"Equals ",[346,3242,968],{},[394,3244,3245,3250,3253],{},[415,3246,3247],{},[346,3248,3249],{},"stoatflow.ha.token.epoch",[415,3251,3252],{},"Gauge — current promotion-token epoch. Increments once per election.",[415,3254,3255],{},"Climbing rapidly (repeated elections \u002F flapping).",[394,3257,3258,3263,3270],{},[415,3259,3260],{},[346,3261,3262],{},"stoatflow.ha.election.outcome",[415,3264,3265,3266,3269],{},"Counter, tagged ",[346,3267,3268],{},"outcome=won|reclaimed|stood_down_fresh|stood_down_lost"," — cumulative election outcomes.",[415,3271,3272,3273,3276],{},"Frequent ",[346,3274,3275],{},"stood_down_lost"," (contended elections).",[394,3278,3279,3284,3290],{},[415,3280,3281],{},[346,3282,3283],{},"stoatflow.ha.election.latency.ms",[415,3285,3286,3287,3289],{},"Gauge — last claim→ACTIVE promotion latency (",[346,3288,1406],{}," until the first).",[415,3291,3292],{},"Above your failover SLO.",[394,3294,3295,3300,3305],{},[415,3296,3297],{},[346,3298,3299],{},"stoatflow.ha.restart.required",[415,3301,3302,3304],{},[346,3303,968],{}," when this pod needs a restart (for example, after the source topic's partition count changes).",[415,3306,3240,3307,1002],{},[346,3308,968],{},[320,3310,3311,3312,3315,3316,3319,3320,3323,3324,1557,3326,3328],{},"A healthy cluster shows exactly one pod with ",[346,3313,3314],{},"role = 1",", its standbys' replication lag hovering near zero, ",[346,3317,3318],{},"peers = N-1",", and ",[346,3321,3322],{},"redundancy.below.desired = 0",". See ",[451,3325,195],{"href":196},[451,3327,243],{"href":244}," for the full surface.",[383,3330,3332],{"id":3331},"failover-characteristics","Failover characteristics",[336,3334,3335,3344],{},[339,3336,3337,3340,3341,3343],{},[324,3338,3339],{},"Graceful"," (rolling deploy or ",[346,3342,2345],{},"): about one handoff — the active drains and commits, an elected standby promotes, and the old active rebuilds as a standby in place (no pod restart). No reprocessing under exactly-once.",[339,3345,3346,3349,3350,3353],{},[324,3347,3348],{},"Ungraceful"," (the active crashes): bounded by detection (",[346,3351,3352],{},"staleness-misses × heartbeat-ms",") plus a near-instant promotion, since the elected standby is already warm. This is independent of state size — the cold-restore window is exactly what hot standby removes.",[320,3355,3356,3357,3360,3361,3364,3365,3368,3369,3372,3373,3368,3376,3379,3380,1002],{},"Segmented window\u002Fsession stores run with the WAL off and would otherwise be durable only on a clean ",[346,3358,3359],{},"close()",". A periodic checkpoint bounds how much of the changelog a pod re-applies after an ",[533,3362,3363],{},"ungraceful"," restart — and how much a promoting standby drains — to at most one checkpoint interval. Tune it with ",[346,3366,3367],{},"stoatflow.state.segment-checkpoint-records"," (default ",[346,3370,3371],{},"100000",") and ",[346,3374,3375],{},"stoatflow.state.segment-checkpoint-ms",[346,3377,3378],{},"30000","), whichever fires first; see the ",[451,3381,3383],{"href":3382},"\u002Fdocs\u002Freference\u002Fconfiguration-reference#stoatflowstate","configuration reference",[320,3385,3386,3387,1002],{},"For measured cold-start numbers on the default (fast-restart) tier — the cost you avoid with hot standby — see ",[451,3388,3390],{"href":3389},"\u002Fproduct\u002Fbenchmarks","Benchmarks",[383,3392,3394],{"id":3393},"limitations-and-what-to-consider","Limitations and what to consider",[336,3396,3397,3403,3420,3430,3436,3446,3455,3464,3470],{},[339,3398,3399,3402],{},[324,3400,3401],{},"It's redundancy, not scale."," Hot standby keeps warm spares; it does not add throughput. There is still exactly one active instance. To go faster, scale the active vertically.",[339,3404,3405,3408,3409,3412,3413,3415,3416,3419],{},[324,3406,3407],{},"More standbys, more footprint and more changelog reads."," Each standby is another always-on instance with its own persistent volume, and another consumer streaming the changelog — so ",[346,3410,3411],{},"K"," standbys multiply that read fan-out ",[346,3414,3411],{},"-fold. ",[346,3417,3418],{},"max-standbys"," bounds it; pick the redundancy you need, not the maximum. The standbys run under the same application licence — they don't add seats.",[339,3421,3422,3425,3426,3429],{},[324,3423,3424],{},"The standbys are not queryable."," While passive a standby only mirrors the changelog into local state; interactive queries (",[346,3427,3428],{},"readOnlyStore",", the state HTTP endpoints) are served by the active alone. A standby answers reads only once it has promoted.",[339,3431,3432,3435],{},[324,3433,3434],{},"Give the active CPU headroom."," A busy active under a fan-out workload needs its cores; standbys co-located with the active on one node compete for them, and a starved active can miss a commit window (handled cleanly, but it's still a restart). Spread the pods across nodes, or size the node for the whole cluster.",[339,3437,3438,3441,3442,3445],{},[324,3439,3440],{},"An extra internal topic."," The coordination topic is auto-created and compacted; account for it when planning topics and ACLs. See ",[451,3443,1277],{"href":3444},"#coordination-topic"," for the principal's permissions, the single-partition and exclusive-per-application requirements, and the pre-create spec for locked-down clusters.",[339,3447,3448,3451,3452,1002],{},[324,3449,3450],{},"At-least-once is bounded-duplicate",", not split-brain-proof — see ",[451,3453,835],{"href":3454},"#processing-guarantees-under-failover",[339,3456,3457,3460,3461,3463],{},[324,3458,3459],{},"Increasing a source topic's partition count requires a restart"," of the cluster; StoatFlow surfaces this via ",[346,3462,3299],{}," and the liveness probe, so Kubernetes recycles the pod.",[339,3465,3466,3469],{},[324,3467,3468],{},"Single region."," Every pod must reach the same Kafka cluster; hot standby is not a multi-region or active-active mechanism.",[339,3471,3472,3475],{},[324,3473,3474],{},"The default is often the right answer."," If a restart-window gap is tolerable, fast restart is simpler and cheaper.",[383,3477,3479],{"id":3478},"next-steps","Next steps",[336,3481,3482,3489,3496,3503],{},[339,3483,3484,3488],{},[324,3485,3486],{},[451,3487,1498],{"href":229}," — the full single-replica manifest this page extends.",[339,3490,3491,3495],{},[324,3492,3493],{},[451,3494,2281],{"href":239}," — liveness versus readiness and how the readiness gate works.",[339,3497,3498,3502],{},[324,3499,3500],{},[451,3501,243],{"href":244}," — scraping metrics and what to alert on.",[339,3504,3505,3510],{},[324,3506,3507],{},[451,3508,3509],{"href":53},"Exactly-once"," — the guarantee that makes failover split-brain-proof.",[3512,3513,3514],"style",{},"html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .su6XF, html code.shiki .su6XF{--shiki-default:#998418}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}html pre.shiki code .sSP4y, html code.shiki .sSP4y{--shiki-default:#B5695977}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}html pre.shiki code .s61at, html code.shiki .s61at{--shiki-default:#99841877}html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}",{"title":558,"searchDepth":572,"depth":572,"links":3516},[3517,3518,3519,3520,3523,3524,3525,3526,3527,3528,3529],{"id":385,"depth":572,"text":386},{"id":474,"depth":572,"text":475},{"id":834,"depth":572,"text":835},{"id":873,"depth":572,"text":150,"children":3521},[3522],{"id":1276,"depth":578,"text":1277},{"id":1481,"depth":572,"text":1482},{"id":2226,"depth":572,"text":2227},{"id":2295,"depth":572,"text":2296},{"id":3077,"depth":572,"text":3078},{"id":3331,"depth":572,"text":3332},{"id":3393,"depth":572,"text":3394},{"id":3478,"depth":572,"text":3479},"Two HA tiers for StoatFlow — fast restart by default, or an opt-in hot-standby cluster (one active plus one or more warm standbys) with near-instant, lag-aware failover — how to choose, configure, deploy, and operate each.","md",{},{"icon":236},"how-to",{"title":233,"description":3530},"U034R67a6DcMTYvZWaUu5IdZKt-VeEXBjVxW23Uw2HQ",[3538,3540],{"title":228,"path":229,"stem":230,"description":3539,"icon":231,"children":-1},"Deploy a StoatFlow application as a single-replica StatefulSet — persistent state volume, termination grace aligned with shutdown, secure license injection, and a representative manifest.",{"title":238,"path":239,"stem":240,"description":3541,"icon":241,"children":-1},"Wire StoatFlow's \u002Fhealth\u002Flive and \u002Fhealth\u002Fready endpoints to Kubernetes probes — what readiness gates, why a frozen commit pipeline fails liveness, and how to tune probe periods.",1783713349445]