[{"data":1,"prerenderedAt":1164},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Ftags\u002Fannouncement":314},[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",[315,705],{"id":316,"title":317,"authors":318,"badge":324,"body":326,"date":694,"description":695,"draft":37,"extension":696,"image":697,"meta":699,"navigation":700,"path":701,"seo":702,"stem":703,"__hash__":704},"posts\u002F3.blog\u002F1.introducing-stoatflow.md","StoatFlow: Kafka Streams compatible engine built to scale up — not out",[319],{"name":320,"to":321,"avatar":322},"Hartmut Armbruster","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fhartmutarmbruster",{"src":323},"\u002Fassets\u002Fhartmut_armbruster_monochromatic.jpg",{"label":325},"Announcement",{"type":327,"value":328,"toc":684},"minimark",[329,379,382,390,399,404,407,410,433,437,444,453,456,470,473,503,507,510,525,532,573,580,584,587,613,623,627,630,660,667,671,674],[330,331,332,339],"blockquote",{},[333,334,335],"p",{},[336,337,338],"strong",{},"TL;DR",[340,341,342,349,355,361,367],"ul",{},[343,344,345,348],"li",{},[336,346,347],{},"What:"," Single-replica JVM stream processor with the Kafka Streams DSL — JDK 25, Project Loom virtual threads.",[343,350,351,354],{},[336,352,353],{},"Kafka Streams DSL compatibility:"," Drop-in — existing topology code ports with a dependency swap.",[343,356,357,360],{},[336,358,359],{},"Why:"," up to 3.4× less CPU and 7.8× less container memory than Kafka Streams on the same hardware; up to 13.6× lower P99 latency on stateful workloads.",[343,362,363,366],{},[336,364,365],{},"The catch:"," one instance per app, so no horizontal scale-out. A single 8-vCPU machine saturates around 200–300 MB\u002Fs of uncompressed throughput — well above most stream-processing workloads, but a real ceiling.",[343,368,369,372,373,378],{},[336,370,371],{},"Access:"," Private alpha — ",[374,375,377],"a",{"href":376},"\u002Fcontact","reach out"," for early access.",[333,380,381],{},"Most stream-processing workloads fit on a single machine. Kafka Streams and Flink scale them out anyway — and you pay the architectural cost of distribution whether your workload needs it or not.",[333,383,384,385,389],{},"StoatFlow is the alternative for the workloads that don't. Same Kafka Streams DSL, one replica per app, built on JDK 25 virtual threads: your existing topology code compiles against StoatFlow, and your operators stop paying the ",[386,387,388],"em",{},"distribution tax",".",[333,391,392,393,396,397,389],{},"For the ",[386,394,395],{},"how",", head to ",[374,398,5],{"href":6},[400,401,403],"h2",{"id":402},"what-we-set-out-to-fix","What we set out to fix",[333,405,406],{},"Stream processing on the JVM gives you two well-known choices: Kafka Streams or Apache Flink. Both are remarkable. Both also scale horizontally by default — which is where most of their architectural and operational complexity comes from.",[333,408,409],{},"Three problems compound:",[340,411,412,418,424],{},[343,413,414,417],{},[336,415,416],{},"Hard to build, harder to run."," Stateful joins, exactly-once, watermarks on out-of-order streams — each is a deep practice. Production then layers on rebalance storms, restart loops, checkpoint failures, and state migrations that miss SLAs.",[343,419,420,423],{},[336,421,422],{},"Every layer is a decision."," Which Kafka client knobs to tune? What about RocksDB? StatefulSets, persistent volumes, static group membership, standby replicas? Deploy on Kubernetes without downtime? You answer all of it before your first event flows.",[343,425,426,429,430,432],{},[336,427,428],{},"Most workloads don't need to scale out."," A workload that comfortably fits on one modern machine pays the ",[386,431,388],{}," for capacity it will never use.",[400,434,436],{"id":435},"a-different-approach","A different approach",[333,438,439,440,443],{},"StoatFlow runs as ",[336,441,442],{},"exactly one instance per application",". No consumer-group rebalancing — there's no group. No state migration — state lives on the instance that owns it. No repartition topics — key-changing operations route through in-memory queues to other lanes inside the same process.",[333,445,446],{},[447,448],"img",{"alt":449,"className":450,"src":452},"StoatFlow high-level architecture: Kafka consumer (all partitions) → lane dispatcher → N virtual-thread lanes with global state → transactional producer.",[451],"rounded-lg","\u002Fassets\u002Fdocs\u002Farchitecture\u002FStoatFlow_high-level_architecture_detailed_20260517.png",[333,454,455],{},"The single-replica bet rests on two recent shifts:",[340,457,458,464],{},[343,459,460,463],{},[336,461,462],{},"Modern JVM concurrency."," Virtual threads (GA in JDK 21) give you thousands of concurrent lanes without platform-thread overhead. StoatFlow targets JDK 25 — virtual threads, structured concurrency, and the Foreign Function & Memory API all in.",[343,465,466,469],{},[336,467,468],{},"Modern hardware."," 16-vCPU compute-optimised instances, 10+ Gbps networking, NVMe storage — off-the-shelf on every major cloud.",[333,471,472],{},"From there, the model is a few moving parts that fit together:",[340,474,475,482,489,496],{},[343,476,477,478,481],{},"Records dispatch to ",[336,479,480],{},"key-affinity lanes"," by consistent hashing — same key, same lane, ordered. Lane count is decoupled from Kafka partition count, so parallelism scales with cores, not partitions.",[343,483,484,485,488],{},"Key-changing operations stay ",[336,486,487],{},"in-process"," — they route to another lane through an in-memory queue, with no repartition-topic round-trip through the broker.",[343,490,491,492,495],{},"State (RocksDB or in-memory) is ",[336,493,494],{},"globally accessible"," to any virtual thread, by any key — layered with epoch buffers for read-your-writes consistency.",[343,497,498,499,502],{},"Exactly-once flows through ",[336,500,501],{},"commit barriers"," that sweep every lane and align with a single Kafka transaction.",[400,504,506],{"id":505},"your-topologies-port-unchanged","Your topologies port unchanged",[333,508,509],{},"The DSL is the one Kafka Streams users already know: KStream, KTable, joins (primary-key, foreign-key, windowed), count \u002F reduce \u002F aggregate \u002F cogroup, tumbling \u002F hopping \u002F session windows with grace and suppression, versioned state stores, the Processor API, and interactive queries — all there.",[333,511,512,515,516,520,521,524],{},[336,513,514],{},"Existing topologies port with a dependency swap and a config cleanup."," Your ",[517,518,519],"code",{},"StreamsBuilder",", your operations, your ",[517,522,523],{},"Materialized"," definitions all compile against StoatFlow. What you delete is the multi-instance scaffolding: standby replicas, stream-thread counts, partition-aware tuning.",[333,526,527,528,531],{},"And ",[336,529,530],{},"on top of the DSL",", StoatFlow ships primitives Kafka Streams doesn't:",[340,533,534,544,551,556,567],{},[343,535,536,537,540,541],{},"Flink-style event-time and processing-time ",[336,538,539],{},"timers"," from any ",[517,542,543],{},"Processor",[343,545,546,547,550],{},"Flink-style ",[336,548,549],{},"watermarks"," with idleness alignment",[343,552,553,555],{},[336,554,140],{}," — topology-level emitters on an interval or cron",[343,557,558,559,562,563,566],{},"Atomic store operations (",[517,560,561],{},"compute",", ",[517,564,565],{},"merge",")",[343,568,569,572],{},[336,570,571],{},"KeyLockManager"," — atomic sections across multiple keys and stores",[333,574,575,576,389],{},"For the full surface, see ",[374,577,579],{"href":578},"\u002Fproduct\u002Ffeatures","Features",[400,581,583],{"id":582},"the-numbers","The numbers",[333,585,586],{},"Benchmarked against Kafka Streams 4.1.1 on a Hetzner 8-vCPU machine — identical topologies, throughput parity:",[340,588,589,595,601,607],{},[343,590,591,592],{},"P99 latency — up to ",[336,593,594],{},"13.6× lower",[343,596,597,598],{},"CPU — up to ",[336,599,600],{},"3.4× less",[343,602,603,604],{},"Container memory — up to ",[336,605,606],{},"7.8× less",[343,608,609,610],{},"State restoration — ",[336,611,612],{},"1.45 to 1.65× faster",[333,614,615,616,622],{},"The ",[374,617,621],{"href":618,"rel":619},"https:\u002F\u002Fstoatflow.io\u002Fproduct\u002Fbenchmarks",[620],"nofollow","full benchmark report"," walks every scenario — topology, infrastructure stack, serdes (String \u002F Avro \u002F Protobuf), load rates, and event-size distributions — so you can compare against the workloads you actually run.",[400,624,626],{"id":625},"where-stoatflow-fits-and-where-it-doesnt","Where StoatFlow fits — and where it doesn't",[333,628,629],{},"One instance per app is a deliberate trade, and it has edges worth stating plainly.",[340,631,632,642,648,654],{},[343,633,634,637,638,641],{},[336,635,636],{},"There's a single-machine ceiling, and we name it."," On that Hetzner 8-core VM, benchmarks measure a ",[336,639,640],{},"200–300 MB\u002Fs uncompressed network-bandwidth ceiling"," — roughly ~124K events\u002Fsec on a 1KB stateless transform, up to ~2.1M events\u002Fsec output on word-count-style aggregation. High-end hardware (96+ cores, faster NICs) hasn't been benchmarked yet.",[343,643,644,647],{},[336,645,646],{},"No horizontal scale-out — by design."," If a workload genuinely needs to span machines, that's not a StoatFlow workload; Kafka Streams and Flink remain the right answer.",[343,649,650,653],{},[336,651,652],{},"State migration is a reprocess, not a restore."," The recommended path onto StoatFlow is to reprocess your input topics — direct restoration from existing Kafka Streams changelog topics isn't supported. If your input retention rules that out, get in touch.",[343,655,656,659],{},[336,657,658],{},"This post doesn't cover everything."," Failover behaviour, cold-start times, and how representative the benchmark scenarios are for your workload are all fair questions — and the docs are still being written.",[333,661,662,663,666],{},"All of this is where the 1.0.0 alpha stands today, not where it's headed. The single-replica design is deliberate and here to stay — but how far one replica goes is exactly what we keep pushing. Benchmarking higher-end hardware to lift the throughput ceiling, shortening cold starts, widening the scenarios we measure, and ongoing refactoring and tuning for more performance on the same hardware are all in flight; the numbers above are a starting point, not a finish line. Scaling ",[386,664,665],{},"up"," harder is the point — expect these edges to move.",[400,668,670],{"id":669},"get-early-access","Get early access",[333,672,673],{},"StoatFlow is in private alpha — distribution is invite-only while we work directly with each early-access team.",[333,675,676,679,680,389],{},[374,677,678],{"href":376},"Reach out"," to request alpha or beta access — especially if you're running stateful Kafka Streams in production today. For release news and updates, ",[374,681,683],{"href":321,"rel":682},[620],"follow on LinkedIn",{"title":685,"searchDepth":686,"depth":686,"links":687},"",2,[688,689,690,691,692,693],{"id":402,"depth":686,"text":403},{"id":435,"depth":686,"text":436},{"id":505,"depth":686,"text":506},{"id":582,"depth":686,"text":583},{"id":625,"depth":686,"text":626},{"id":669,"depth":686,"text":670},"2026-05-17","The first alpha is here. Kafka Streams DSL on a single-replica runtime with virtual-thread parallelism — measurably less CPU, memory, and latency on the same hardware.","md",{"src":698},"\u002Fassets\u002Fblog\u002Fog\u002FStoatFlow_og-2_20260517.jpg",{},true,"\u002Fblog\u002Fintroducing-stoatflow",{"title":317,"description":695},"3.blog\u002F1.introducing-stoatflow","gIebdxc3AXAX4qiFiwVeTK3gX-hOnbbxbeKnENy8yT0",{"id":706,"title":707,"authors":708,"badge":711,"body":712,"date":1156,"description":1157,"draft":37,"extension":696,"image":1158,"meta":1159,"navigation":700,"path":1160,"seo":1161,"stem":1162,"__hash__":1163},"posts\u002F3.blog\u002F4.hot-standby-high-availability.md","Hot standby for StoatFlow: failover in seconds, not a cold restore",[709],{"name":320,"to":321,"avatar":710},{"src":323},{"label":325},{"type":327,"value":713,"toc":1145},[714,717,723,766,770,820,823,829,837,844,847,908,911,918,994,997,1007,1021,1024,1027,1030,1037,1050,1053,1056,1097,1100,1119,1132,1135,1138],[333,715,716],{},"A StoatFlow application recovers by restarting: the process dies, the orchestrator brings it back, and local state rebuilds from the Kafka changelog up to the last committed barrier — no data loss, no duplicate output. That is fast, and for most workloads it is enough. But the rebuild reads the changelog, so recovery time scales with how much state there is. At tens of gigabytes, a cold restore can run into minutes.",[333,718,719,720,722],{},"Hot standby removes that scaling. It is an opt-in active\u002Fpassive pair where a warm standby takes over in seconds, independent of state size — and under exactly-once, with no duplicates across the handoff. The full operator guide is on ",[374,721,233],{"href":234},"; here is why it exists and what it gives you.",[330,724,725,729],{},[333,726,727],{},[336,728,338],{},[340,730,731,736,741,747,753,758],{},[343,732,733,735],{},[336,734,347],{}," Hot standby — an opt-in active\u002Fpassive pair. Off by default; the single-instance runtime is byte-for-byte unchanged when you don't enable it.",[343,737,738,740],{},[336,739,359],{}," Recovery has always been fast restart, but a cold rebuild from the changelog scales with state size. For large state or a tight recovery-time objective, that window is too long.",[343,742,743,746],{},[336,744,745],{},"How:"," A passive standby continuously follows the changelog and stays warm. Promotion is seconds, independent of state size — no cold restore on the critical path.",[343,748,749,752],{},[336,750,751],{},"Guarantee:"," Under exactly-once, broker-enforced transactional fencing makes split-brain impossible — at most one instance ever commits. Under at-least-once, failover is bounded-duplicate.",[343,754,755,757],{},[336,756,365],{}," It is redundancy, not scale. Still one active instance, roughly double the footprint. To go faster, scale up, not out.",[343,759,760,763,764,389],{},[336,761,762],{},"Docs:"," ",[374,765,233],{"href":234},[400,767,769],{"id":768},"in-this-post","In this post",[340,771,772,778,784,790,796,802,808,814],{},[343,773,774],{},[374,775,777],{"href":776},"#why-this-exists","Why this exists",[343,779,780],{},[374,781,783],{"href":782},"#what-hot-standby-gives-you","What hot standby gives you",[343,785,786],{},[374,787,789],{"href":788},"#fast-restart-vs-hot-standby","Fast restart vs hot standby",[343,791,792],{},[374,793,795],{"href":794},"#how-it-works","How it works",[343,797,798],{},[374,799,801],{"href":800},"#exactly-once-across-failover","Exactly-once across failover",[343,803,804],{},[374,805,807],{"href":806},"#tradeoffs-and-limits","Tradeoffs and limits",[343,809,810],{},[374,811,813],{"href":812},"#running-it","Running it",[343,815,816],{},[374,817,819],{"href":818},"#when-to-turn-it-on","When to turn it on",[400,821,777],{"id":822},"why-this-exists",[333,824,825,826,828],{},"StoatFlow runs as exactly one instance per application. That is the whole point: no consumer-group rebalancing, no state migration, no repartition topics — you stop paying the ",[386,827,388],{}," for scale-out you don't need. Availability in that model comes from fast restart, and fast restart is a credible story because there is no rebalancing to wait out: restart cost is restoration cost, full stop.",[333,830,831,832,836],{},"The cost, though, is bounded by state size. On small-to-medium state the engine is processing again quickly — the ",[374,833,835],{"href":834},"\u002Fproduct\u002Fbenchmarks","benchmarks"," measure cold-start across representative workloads, and a persistent volume narrows it further to just the changelog gap since the last commit. But a cold start still reads the changelog, and a large store takes longer to replay than a small one. For a workload carrying tens of gigabytes of state with a tight recovery-time objective, minutes of recovery on an unplanned restart is the line between acceptable and not.",[333,838,839,840,843],{},"When we shipped the ",[374,841,842],{"href":701},"first alpha",", failover behaviour was a fair question we said we hadn't answered yet. This is the answer: an availability tier for the workloads that can't absorb a cold-restore window — without giving up the single-instance model, and without reintroducing the distribution tax. There is still exactly one active instance. There is still no rebalancing and no horizontal scale. There is now a warm spare.",[400,845,783],{"id":846},"what-hot-standby-gives-you",[340,848,849,855,861,871,877,883],{},[343,850,851,854],{},[336,852,853],{},"Failover in seconds, independent of state size."," The standby is already warm, so taking over is not a restore — it is a handoff. The cold-start window that scales with your state is exactly what this removes.",[343,856,857,860],{},[336,858,859],{},"Near-zero-downtime rolling deploys."," A version roll costs about one graceful handoff: the active drains, commits, and hands over to the standby. No rebalance, no cluster-wide reconvergence.",[343,862,863,866,867,870],{},[336,864,865],{},"Exactly-once preserved across the handoff."," The recovery anchor is the last committed barrier whether you restart cold or fail over to a standby. Under exactly-once there are no duplicates downstream for ",[517,868,869],{},"read_committed"," consumers.",[343,872,873,876],{},[336,874,875],{},"An order-independent rollout, with no custom controller."," Readiness gates the roll: a standby reports ready only once it has caught up, and a catching-up standby is not killed mid-catch-up. Kubernetes never advances the roll onto an instance that isn't ready — whichever pod rolls first.",[343,878,879,882],{},[336,880,881],{},"Opt-in, with the default untouched."," Hot standby is off unless you turn it on. Leave it off and you get exactly today's single-instance behaviour, with none of the extra moving parts.",[343,884,885,888,889,892,893,562,896,899,900,903,904,907],{},[336,886,887],{},"Observable and operable."," A ",[517,890,891],{},"\u002Fha\u002Fstatus"," endpoint reports each pod's role, replication lag, and the peers it sees; ",[517,894,895],{},"\u002Fha\u002Fswitch",[517,897,898],{},"\u002Fha\u002Fpromote",", and ",[517,901,902],{},"\u002Fha\u002Fdemote"," drive a controlled handoff; and the pair exports ",[517,905,906],{},"stoatflow.ha.*"," metrics for dashboards and alerts.",[400,909,789],{"id":910},"fast-restart-vs-hot-standby",[333,912,913,914,917],{},"Neither tier involves two ",[386,915,916],{},"active"," instances — that remains the rule. You choose per application.",[919,920,921,936],"table",{},[922,923,924],"thead",{},[925,926,927,930,933],"tr",{},[928,929],"th",{},[928,931,932],{},"Fast restart (default)",[928,934,935],{},"Hot standby (opt-in)",[937,938,939,951,962,973,983],"tbody",{},[925,940,941,945,948],{},[942,943,944],"td",{},"Model",[942,946,947],{},"One instance",[942,949,950],{},"Active\u002Fpassive pair",[925,952,953,956,959],{},[942,954,955],{},"Recovery on failure",[942,957,958],{},"Restart + rebuild from changelog",[942,960,961],{},"Promote the warm standby",[925,963,964,967,970],{},[942,965,966],{},"Failover time",[942,968,969],{},"Scales with state size",[942,971,972],{},"Seconds, independent of state size",[925,974,975,978,980],{},[942,976,977],{},"Cost",[942,979,947],{},[942,981,982],{},"~2× — a second always-on instance + its volume",[925,984,985,988,991],{},[942,986,987],{},"Choose it for",[942,989,990],{},"Most workloads",[942,992,993],{},"Large state or a tight recovery-time objective",[400,995,795],{"id":996},"how-it-works",[333,998,999,1000,1002,1003,1006],{},"At any moment one instance is the ",[336,1001,916],{}," — it owns the source partitions, processes records, and commits — and the other is a ",[336,1004,1005],{},"passive standby",". The standby consumes the same changelog the active writes, keeping its own local state warm and a step behind; it does not process source records or produce output. The two coordinate through a compacted internal Kafka topic, so each knows the other is alive and which role it holds.",[333,1008,1009,1010,1013,1014,1016,1017,1020],{},"Promotion happens two ways. A ",[336,1011,1012],{},"graceful handoff"," — a rolling deploy or an explicit ",[517,1015,895],{}," — drains the active, commits, and hands the role over. A ",[336,1018,1019],{},"failover"," — the active crashes — is detected when the standby stops seeing the active's heartbeats, and the standby promotes itself. Because it is already warm, taking over does not wait on a cold restore.",[333,1022,1023],{},"The safety property underneath is Kafka's own. Under exactly-once, a transactional producer is fenced by its epoch: if a network partition ever left two instances briefly believing they are active, the broker lets only one commit and fences the other, which then shuts itself down. Correctness does not depend on the failure detector being perfect — it depends on the fence, which the broker enforces.",[400,1025,801],{"id":1026},"exactly-once-across-failover",[333,1028,1029],{},"How clean a failover is depends on the processing guarantee the application runs under — and the two behave differently enough to call out.",[333,1031,1032,1033,1036],{},"Under ",[336,1034,1035],{},"exactly-once"," (the default), failover is split-brain-proof. The transactional fence guarantees at most one instance can ever commit a given transaction, so the handoff produces no duplicates and loses no committed work. There is nothing extra to do downstream.",[333,1038,1032,1039,1042,1043,1046,1047,389],{},[336,1040,1041],{},"at-least-once",", failover is ",[386,1044,1045],{},"bounded-duplicate",". The pair still coordinates promotion safely, but without transactional fencing an ungraceful failover can re-process a small, bounded window of records, so a few duplicate output records are possible. If you run hot standby under at-least-once, make downstream consumers idempotent or duplicate-tolerant. The guarantee model is covered on ",[374,1048,1049],{"href":53},"Exactly-once",[400,1051,807],{"id":1052},"tradeoffs-and-limits",[333,1054,1055],{},"Hot standby is a deliberate trade, and it has edges worth stating plainly.",[340,1057,1058,1064,1070,1076,1085,1091],{},[343,1059,1060,1063],{},[336,1061,1062],{},"It is redundancy, not scale."," A warm spare does not add throughput. There is still exactly one active instance — to go faster, give it more cores and memory.",[343,1065,1066,1069],{},[336,1067,1068],{},"Roughly double the footprint."," A second always-on instance, its own persistent volume, and the licence that goes with it.",[343,1071,1072,1075],{},[336,1073,1074],{},"At-least-once is bounded-duplicate",", not split-brain-proof. Exactly-once is the guarantee that makes a handoff clean.",[343,1077,1078,1081,1082,1084],{},[336,1079,1080],{},"Crash failover is not zero-downtime."," It is bounded by detection plus a near-instant promotion — short, but not zero. A graceful handoff (deploy or ",[517,1083,895],{},") is about one handoff.",[343,1086,1087,1090],{},[336,1088,1089],{},"Single region."," Both instances must reach the same Kafka cluster; this is not a multi-region or active-active mechanism.",[343,1092,1093,1096],{},[336,1094,1095],{},"The default is often the right answer."," If a cold-restore window of seconds-to-minutes is acceptable, fast restart is simpler and cheaper. Hot standby exists to remove that window, not because the default is unsafe.",[400,1098,813],{"id":1099},"running-it",[333,1101,1102,1103,1106,1107,1110,1111,1114,1115,1118],{},"You deploy the pair as a two-replica ",[517,1104,1105],{},"StatefulSet"," — the same single-replica manifest you already use, with ",[517,1108,1109],{},"replicas: 2",", a per-pod persistent volume, and a rolling update. The split ",[517,1112,1113],{},"\u002Fhealth\u002Flive"," and ",[517,1116,1117],{},"\u002Fhealth\u002Fready"," probes are what make the roll order-independent, and a termination grace period long enough for the active to finish its handoff keeps deploys clean.",[333,1120,1121,1122,1125,1126,1128,1129,1131],{},"Turning it on is one configuration knob: ",[517,1123,1124],{},"stoatflow.ha.mode: active-standby",". The rest — the coordination topic, the per-pod identity, the staleness and lag thresholds — has sensible defaults; on Kubernetes a typical deployment sets only the mode. ",[374,1127,233],{"href":234}," has the manifest, the configuration reference, and the operator endpoints; ",[374,1130,228],{"href":229}," covers the deployment shape it extends.",[400,1133,819],{"id":1134},"when-to-turn-it-on",[333,1136,1137],{},"Reach for hot standby when a cold-restore window is the thing you can't afford — large state, a tight recovery-time objective, or a deploy cadence where even a fast restart's gap shows up downstream. For everything else, the single-instance default remains the simpler, cheaper choice, and it is unchanged whether hot standby exists or not.",[333,1139,1140,1141,1144],{},"This is the first cut of the failover story, and it is built to get smarter: promotion selection becomes lag-aware so the freshest standby wins, and the readiness threshold becomes a tunable rather than a binary. The model — one active, a warm passive spare, the fence as the backstop — is the part that stays. Start with the ",[374,1142,1143],{"href":234},"High availability guide",", and if you're running large-state stateful Kafka Streams in production today, that is exactly the workload this was built for.",{"title":685,"searchDepth":686,"depth":686,"links":1146},[1147,1148,1149,1150,1151,1152,1153,1154,1155],{"id":768,"depth":686,"text":769},{"id":822,"depth":686,"text":777},{"id":846,"depth":686,"text":783},{"id":910,"depth":686,"text":789},{"id":996,"depth":686,"text":795},{"id":1026,"depth":686,"text":801},{"id":1052,"depth":686,"text":807},{"id":1099,"depth":686,"text":813},{"id":1134,"depth":686,"text":819},"2026-06-17","StoatFlow recovery has always been fast restart — but restart time scales with state. Hot standby is an opt-in active\u002Fpassive pair that fails over in seconds regardless of state size, with exactly-once preserved across the handoff. Redundancy, not scale-out.",{"src":698},{},"\u002Fblog\u002Fhot-standby-high-availability",{"title":707,"description":1157},"3.blog\u002F4.hot-standby-high-availability","LsS5ctjtBd2Q51pnj52tQmQ4TKfJGhL-mFzT8ifEQX4",1783713349042]