[{"data":1,"prerenderedAt":1447},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Fthree-clocks-stream-time-vs-watermarks":334,"\u002Fblog\u002Fthree-clocks-stream-time-vs-watermarks-surround":1436},[4,43,91,154,181,224,262,293],{"title":5,"path":6,"stem":7,"children":8,"icon":42},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17,22,27,32,37],{"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",{"title":38,"path":39,"stem":40,"icon":41},"AI assistants","\u002Fdocs\u002Fgetting-started\u002Fai-assistants","1.docs\u002F1.getting-started\u002F7.ai-assistants","i-lucide-bot",false,{"title":44,"path":45,"stem":46,"children":47,"icon":50},"Core Concepts","\u002Fdocs\u002Fconcepts","1.docs\u002F2.concepts\u002F1.index",[48,51,56,61,66,71,76,81,86],{"title":49,"path":45,"stem":46,"icon":50},"Core concepts","i-lucide-book-open",{"title":52,"path":53,"stem":54,"icon":55},"Architecture","\u002Fdocs\u002Fconcepts\u002Farchitecture","1.docs\u002F2.concepts\u002F2.architecture","i-lucide-layers",{"title":57,"path":58,"stem":59,"icon":60},"Exactly-once semantics","\u002Fdocs\u002Fconcepts\u002Fexactly-once","1.docs\u002F2.concepts\u002F3.exactly-once","i-lucide-shield-check",{"title":62,"path":63,"stem":64,"icon":65},"Lanes and parallelism","\u002Fdocs\u002Fconcepts\u002Flanes-and-parallelism","1.docs\u002F2.concepts\u002F4.lanes-and-parallelism","i-lucide-split",{"title":67,"path":68,"stem":69,"icon":70},"State and thread-safety","\u002Fdocs\u002Fconcepts\u002Fstate-and-thread-safety","1.docs\u002F2.concepts\u002F5.state-and-thread-safety","i-lucide-lock",{"title":72,"path":73,"stem":74,"icon":75},"Event time and watermarks","\u002Fdocs\u002Fconcepts\u002Fevent-time-and-watermarks","1.docs\u002F2.concepts\u002F6.event-time-and-watermarks","i-lucide-clock",{"title":77,"path":78,"stem":79,"icon":80},"The configuration model","\u002Fdocs\u002Fconcepts\u002Fconfiguration-model","1.docs\u002F2.concepts\u002F7.configuration-model","i-lucide-sliders-horizontal",{"title":82,"path":83,"stem":84,"icon":85},"The error-handling model","\u002Fdocs\u002Fconcepts\u002Ferror-handling-model","1.docs\u002F2.concepts\u002F8.error-handling-model","i-lucide-triangle-alert",{"title":87,"path":88,"stem":89,"icon":90},"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":92,"path":93,"stem":94,"children":95,"icon":98},"Building Topologies","\u002Fdocs\u002Fbuilding","1.docs\u002F3.building\u002F1.index",[96,99,104,109,114,119,124,129,134,139,144,149],{"title":97,"path":93,"stem":94,"icon":98},"Building topologies","i-lucide-blocks",{"title":100,"path":101,"stem":102,"icon":103},"Error handling and DLQ","\u002Fdocs\u002Fbuilding\u002Ferror-handling-dlq","1.docs\u002F3.building\u002F10.error-handling-dlq","i-lucide-circle-x",{"title":105,"path":106,"stem":107,"icon":108},"State stores","\u002Fdocs\u002Fbuilding\u002Fstate-stores","1.docs\u002F3.building\u002F11.state-stores","i-lucide-database",{"title":110,"path":111,"stem":112,"icon":113},"Testing topologies","\u002Fdocs\u002Fbuilding\u002Ftesting","1.docs\u002F3.building\u002F12.testing","i-lucide-flask-conical",{"title":115,"path":116,"stem":117,"icon":118},"Sources and sinks","\u002Fdocs\u002Fbuilding\u002Fstreams-builder","1.docs\u002F3.building\u002F2.streams-builder","i-lucide-import",{"title":120,"path":121,"stem":122,"icon":123},"KStream and KTable operations","\u002Fdocs\u002Fbuilding\u002Fkstream-ktable","1.docs\u002F3.building\u002F3.kstream-ktable","i-lucide-waypoints",{"title":125,"path":126,"stem":127,"icon":128},"Aggregations","\u002Fdocs\u002Fbuilding\u002Faggregations","1.docs\u002F3.building\u002F4.aggregations","i-lucide-sigma",{"title":130,"path":131,"stem":132,"icon":133},"Windowing","\u002Fdocs\u002Fbuilding\u002Fwindowing","1.docs\u002F3.building\u002F5.windowing","i-lucide-calendar-clock",{"title":135,"path":136,"stem":137,"icon":138},"Joins","\u002Fdocs\u002Fbuilding\u002Fjoins","1.docs\u002F3.building\u002F6.joins","i-lucide-git-merge",{"title":140,"path":141,"stem":142,"icon":143},"The Processor API","\u002Fdocs\u002Fbuilding\u002Fprocessor-api","1.docs\u002F3.building\u002F7.processor-api","i-lucide-cpu",{"title":145,"path":146,"stem":147,"icon":148},"Scheduled sources","\u002Fdocs\u002Fbuilding\u002Fscheduled-sources","1.docs\u002F3.building\u002F8.scheduled-sources","i-lucide-timer",{"title":150,"path":151,"stem":152,"icon":153},"Serdes and Avro","\u002Fdocs\u002Fbuilding\u002Fserdes","1.docs\u002F3.building\u002F9.serdes","i-lucide-binary",{"title":155,"path":156,"stem":157,"children":158,"icon":80},"Configuration","\u002Fdocs\u002Fconfiguration","1.docs\u002F4.configuration\u002F1.index",[159,161,166,171,176],{"title":160,"path":156,"stem":157,"icon":80},"How configuration works",{"title":162,"path":163,"stem":164,"icon":165},"Engine configuration (:core)","\u002Fdocs\u002Fconfiguration\u002Fcore-config","1.docs\u002F4.configuration\u002F2.core-config","i-lucide-settings-2",{"title":167,"path":168,"stem":169,"icon":170},"Runtime configuration (:runtime)","\u002Fdocs\u002Fconfiguration\u002Fruntime-config","1.docs\u002F4.configuration\u002F3.runtime-config","i-lucide-server-cog",{"title":172,"path":173,"stem":174,"icon":175},"Defaults, adaptivity, and presets","\u002Fdocs\u002Fconfiguration\u002Fdefaults-and-presets","1.docs\u002F4.configuration\u002F4.defaults-and-presets","i-lucide-gauge",{"title":177,"path":178,"stem":179,"icon":180},"Kafka client configuration","\u002Fdocs\u002Fconfiguration\u002Fkafka-client-config","1.docs\u002F4.configuration\u002F5.kafka-client-config","i-lucide-plug",{"title":182,"path":183,"stem":184,"children":185,"icon":188},"Running in Production","\u002Fdocs\u002Fruntime","1.docs\u002F5.runtime\u002F1.index",[186,189,194,199,204,209,214,219],{"title":187,"path":183,"stem":184,"icon":188},"The runtime","i-lucide-server",{"title":190,"path":191,"stem":192,"icon":193},"The REST API","\u002Fdocs\u002Fruntime\u002Frest-api","1.docs\u002F5.runtime\u002F2.rest-api","i-lucide-globe",{"title":195,"path":196,"stem":197,"icon":198},"Health checks","\u002Fdocs\u002Fruntime\u002Fhealth-checks","1.docs\u002F5.runtime\u002F3.health-checks","i-lucide-heart-pulse",{"title":200,"path":201,"stem":202,"icon":203},"Metrics","\u002Fdocs\u002Fruntime\u002Fmetrics","1.docs\u002F5.runtime\u002F4.metrics","i-lucide-activity",{"title":205,"path":206,"stem":207,"icon":208},"Pause and resume","\u002Fdocs\u002Fruntime\u002Fpause-unpause","1.docs\u002F5.runtime\u002F5.pause-unpause","i-lucide-pause",{"title":210,"path":211,"stem":212,"icon":213},"Plugins and lifecycle hooks","\u002Fdocs\u002Fruntime\u002Fplugins","1.docs\u002F5.runtime\u002F6.plugins","i-lucide-puzzle",{"title":215,"path":216,"stem":217,"icon":218},"Docker images","\u002Fdocs\u002Fruntime\u002Fdocker","1.docs\u002F5.runtime\u002F7.docker","i-lucide-container",{"title":220,"path":221,"stem":222,"icon":223},"GraalVM native image","\u002Fdocs\u002Fruntime\u002Fnative-image","1.docs\u002F5.runtime\u002F8.native-image","i-lucide-zap",{"title":225,"path":226,"stem":227,"children":228,"icon":231},"Deploying & Operating","\u002Fdocs\u002Foperating","1.docs\u002F6.operating\u002F1.index",[229,232,237,242,247,252,257],{"title":230,"path":226,"stem":227,"icon":231},"Deploying and operating","i-lucide-life-buoy",{"title":233,"path":234,"stem":235,"icon":236},"Running on Kubernetes","\u002Fdocs\u002Foperating\u002Fkubernetes","1.docs\u002F6.operating\u002F2.kubernetes","i-lucide-ship",{"title":238,"path":239,"stem":240,"icon":241},"High availability","\u002Fdocs\u002Foperating\u002Fhigh-availability","1.docs\u002F6.operating\u002F3.high-availability","i-lucide-copy",{"title":243,"path":244,"stem":245,"icon":246},"Liveness and readiness probes","\u002Fdocs\u002Foperating\u002Fprobes","1.docs\u002F6.operating\u002F4.probes","i-lucide-stethoscope",{"title":248,"path":249,"stem":250,"icon":251},"Observability","\u002Fdocs\u002Foperating\u002Fobservability","1.docs\u002F6.operating\u002F5.observability","i-lucide-telescope",{"title":253,"path":254,"stem":255,"icon":256},"Tuning under load","\u002Fdocs\u002Foperating\u002Ftuning","1.docs\u002F6.operating\u002F6.tuning","i-lucide-sliders",{"title":258,"path":259,"stem":260,"icon":261},"Production checklist","\u002Fdocs\u002Foperating\u002Fproduction-checklist","1.docs\u002F6.operating\u002F7.production-checklist","i-lucide-clipboard-check",{"title":263,"path":264,"stem":265,"children":266,"icon":90},"Migrating from Kafka Streams","\u002Fdocs\u002Fmigration","1.docs\u002F7.migration\u002F1.index",[267,268,273,278,283,288],{"title":263,"path":264,"stem":265,"icon":90},{"title":269,"path":270,"stem":271,"icon":272},"Automated port","\u002Fdocs\u002Fmigration\u002Fautomated-port","1.docs\u002F7.migration\u002F2.automated-port","i-lucide-wand-sparkles",{"title":274,"path":275,"stem":276,"icon":277},"Migration without carrying state","\u002Fdocs\u002Fmigration\u002Fwithout-data-migration","1.docs\u002F7.migration\u002F3.without-data-migration","i-lucide-sparkles",{"title":279,"path":280,"stem":281,"icon":282},"Migration carrying state","\u002Fdocs\u002Fmigration\u002Fwith-data-migration","1.docs\u002F7.migration\u002F4.with-data-migration","i-lucide-database-backup",{"title":284,"path":285,"stem":286,"icon":287},"The migration tool","\u002Fdocs\u002Fmigration\u002Fmigration-tool","1.docs\u002F7.migration\u002F5.migration-tool","i-lucide-truck",{"title":289,"path":290,"stem":291,"icon":292},"Reusing your Kafka Streams dashboards","\u002Fdocs\u002Fmigration\u002Freusing-kafka-streams-dashboards","1.docs\u002F7.migration\u002F6.reusing-kafka-streams-dashboards","i-lucide-line-chart",{"title":294,"path":295,"stem":296,"children":297,"icon":299},"Reference","\u002Fdocs\u002Freference","1.docs\u002F8.reference\u002F1.index",[298,300,305,310,315,320,325,329],{"title":294,"path":295,"stem":296,"icon":299},"i-lucide-list",{"title":301,"path":302,"stem":303,"icon":304},"Configuration reference","\u002Fdocs\u002Freference\u002Fconfiguration-reference","1.docs\u002F8.reference\u002F2.configuration-reference","i-lucide-table",{"title":306,"path":307,"stem":308,"icon":309},"REST API reference","\u002Fdocs\u002Freference\u002Frest-api-reference","1.docs\u002F8.reference\u002F3.rest-api-reference","i-lucide-network",{"title":311,"path":312,"stem":313,"icon":314},"Gradle plugin reference","\u002Fdocs\u002Freference\u002Fgradle-plugin-reference","1.docs\u002F8.reference\u002F4.gradle-plugin-reference","i-lucide-box",{"title":316,"path":317,"stem":318,"icon":319},"Maven reference","\u002Fdocs\u002Freference\u002Fmaven-reference","1.docs\u002F8.reference\u002F5.maven-reference","i-simple-icons-apachemaven",{"title":321,"path":322,"stem":323,"icon":324},"Kafka Streams compatibility matrix","\u002Fdocs\u002Freference\u002Fks-compatibility-matrix","1.docs\u002F8.reference\u002F6.ks-compatibility-matrix","i-lucide-table-2",{"title":326,"path":327,"stem":328,"icon":175},"Metrics reference","\u002Fdocs\u002Freference\u002Fmetrics-reference","1.docs\u002F8.reference\u002F7.metrics-reference",{"title":330,"path":331,"stem":332,"icon":333},"Glossary","\u002Fdocs\u002Freference\u002Fglossary","1.docs\u002F8.reference\u002F8.glossary","i-lucide-book-a",{"id":335,"title":336,"authors":337,"badge":343,"body":345,"date":1423,"description":1424,"draft":42,"extension":1425,"image":1426,"meta":1428,"navigation":1429,"ogImage":1430,"path":1431,"seo":1432,"sitemap":1433,"stem":1434,"updated":1430,"__hash__":1435},"posts\u002F3.blog\u002F15.three-clocks-stream-time-vs-watermarks.md","Three clocks: stream time, Flink watermarks, and the one watermark StoatFlow computes",[338],{"name":339,"to":340,"avatar":341},"Hartmut Armbruster","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fhartmut-co-uk\u002F",{"src":342},"\u002Fassets\u002Fhartmut_armbruster_monochromatic.jpg",{"label":344},"Deep Dive",{"type":346,"value":347,"toc":1412},"minimark",[348,366,381,384,387,471,476,526,529,546,632,635,745,748,762,789,803,806,837,851,877,880,919,1079,1086,1102,1138,1141,1148,1172,1185,1188,1195,1202,1210,1213,1219,1254,1266,1274,1290,1293,1299,1305,1319,1327,1333,1339,1342,1353,1358,1408],[349,350,353],"callout",{"color":351,"icon":352},"neutral","i-lucide-list-ordered",[354,355,356,360,361,365],"p",{},[357,358,359],"strong",{},"Event time in StoatFlow — part 1 of 4."," Three clocks ",[362,363,364],"em",{},"(this post)"," · Making the watermark move · Closing a window under exactly-once · Testing and operating event time",[349,367,370],{"color":368,"icon":369},"info","i-lucide-info",[354,371,372,375,376,380],{},[357,373,374],{},"Update, 1.0.0-rc.4."," StoatFlow now keeps one clock, not two: the watermark decides both when a window closes and whether a record is late, as in Flink — every window kind and emit strategy, and it survives restarts. A session closes once the watermark passes its last event plus the inactivity gap plus grace. The per-operator stream time this post describes is gone; see ",[377,378,379],"a",{"href":131},"windowing"," for the current rule.",[354,382,383],{},"Run the same five-minute windowed count over the same records on Kafka Streams, Flink and StoatFlow, and you get three different answers to the question \"is this window done?\" — not because the windows differ, but because each engine keeps a different clock. Kafka Streams' clock is the highest record timestamp a task has seen. Flink's is a watermark that every operator derives for itself as the minimum over its inputs. StoatFlow's is one watermark computed once, at the edge: the minimum across non-idle partitions, minus an out-of-orderness bound, delivered as the same value to every operator in the topology.",[354,385,386],{},"That would be the whole post, except that StoatFlow keeps two of those clocks, not one. It accepts a late record the way Kafka Streams does — against the operator's own stream time — and closes the window the way Flink does, on the watermark. Getting that right matters for a ported application, and it matters for us: our compatibility matrix said something different, and the first version of the correction we published was wrong too. This post sets the three clocks side by side, walks one arrival sequence through all three, and states exactly what a Kafka Streams application sees when it starts reading StoatFlow's.",[388,389,390,395],"blockquote",{},[354,391,392],{},[357,393,394],{},"TL;DR",[396,397,398,405,420,430,440,449,459],"ul",{},[399,400,401,404],"li",{},[357,402,403],{},"What:"," Three definitions of event-time \"now\". Kafka Streams — per task, the maximum record timestamp seen, optimistic. Flink — per operator, the minimum watermark over its inputs, pessimistic and distributed. StoatFlow — one global watermark, the minimum across non-idle partitions minus the out-of-orderness bound, computed at the edge.",[399,406,407,410,411,415,416,419],{},[357,408,409],{},"Why it matters:"," the clock decides when a window's final result is emitted, when ",[412,413,414],"code",{},"suppress"," releases, when an event-time timer fires, and what ",[412,417,418],{},"currentStreamTimeMs()"," returns.",[399,421,422,425,426,429],{},[357,423,424],{},"How StoatFlow does it:"," Flink's ",[412,427,428],{},"WatermarkStrategy"," API per source, one generator per partition, one minimum on the dispatch path — and, inside every windowed operator, a Kafka Streams-style stream time that decides whether a late record is still accepted.",[399,431,432,435,436,439],{},[357,433,434],{},"The guarantee:"," every operator sees the same watermark in the same epoch, so there is no per-operator disagreement about what is late. A Kafka Streams application ported to StoatFlow counts the ",[357,437,438],{},"same"," records into each window; only the instant the final result appears changes.",[399,441,442,445,446,448],{},[357,443,444],{},"The catch:"," that instant is later — by the out-of-orderness bound at least, and by up to two commit intervals on top of it (part 3). One lagging, non-idle partition delays every window in the application. ",[412,447,418],{}," returns the watermark, which is lower than Kafka Streams stream time and global rather than per-task.",[399,450,451,454,455,458],{},[357,452,453],{},"The correction:"," Kafka Streams grace has bounded ",[362,456,457],{},"acceptance",", not retention, since KIP-328 — and StoatFlow's acceptance rule is the same one. The difference between the engines is the clock that closes the window, not the rule that admits a record.",[399,460,461,464,465,467,468,470],{},[357,462,463],{},"Docs:"," ",[377,466,72],{"href":73}," · ",[377,469,321],{"href":322},".",[472,473,475],"h2",{"id":474},"in-this-post","In this post",[396,477,478,484,490,496,502,508,514,520],{},[399,479,480],{},[377,481,483],{"href":482},"#three-answers-to-is-this-window-done","Three answers to \"is this window done?\"",[399,485,486],{},[377,487,489],{"href":488},"#kafka-streams-one-clock-optimistic-and-per-task","Kafka Streams: one clock, optimistic and per-task",[399,491,492],{},[377,493,495],{"href":494},"#flink-one-clock-pessimistic-and-per-operator","Flink: one clock, pessimistic and per-operator",[399,497,498],{},[377,499,501],{"href":500},"#stoatflow-both-clocks","StoatFlow: both clocks",[399,503,504],{},[377,505,507],{"href":506},"#grace-the-same-rule-the-close-is-what-differs","Grace: the same rule — the close is what differs",[399,509,510],{},[377,511,513],{"href":512},"#what-a-ported-application-sees","What a ported application sees",[399,515,516],{},[377,517,519],{"href":518},"#tradeoffs-and-limits","Tradeoffs and limits",[399,521,522],{},[377,523,525],{"href":524},"#whats-next","What's next",[472,527,483],{"id":528},"three-answers-to-is-this-window-done",[354,530,531,532,534,535,538,539,541,542,545],{},"A windowed aggregation makes two decisions about time, and it is worth keeping them apart from the start. The first is ",[357,533,457],{},": a record arrives for a window whose end has passed — is it folded in, or dropped? The second is the ",[357,536,537],{},"close",": at what moment is the window's result final, so that ",[412,540,414],{}," can release it, an ",[412,543,544],{},"onWindowClose"," emission can fire, and the state can eventually be expired? Each decision needs a clock, and the three engines pair the two decisions with clocks differently.",[547,548,549,564],"table",{},[550,551,552],"thead",{},[553,554,555,558,561],"tr",{},[556,557],"th",{},[556,559,560],{},"Accepts a late record while…",[556,562,563],{},"Closes the window when…",[565,566,567,589,609],"tbody",{},[553,568,569,575,584],{},[570,571,572],"td",{},[357,573,574],{},"Kafka Streams",[570,576,577,580,581],{},[412,578,579],{},"windowEnd + grace"," is still ahead of the task's ",[357,582,583],{},"stream time",[570,585,586,587],{},"stream time passes ",[412,588,579],{},[553,590,591,596,606],{},[570,592,593],{},[357,594,595],{},"Flink",[570,597,598,599,602,603],{},"the operator's ",[357,600,601],{},"watermark"," is still below ",[412,604,605],{},"windowEnd + allowedLateness",[570,607,608],{},"the watermark passes it",[553,610,611,616,623],{},[570,612,613],{},[357,614,615],{},"StoatFlow",[570,617,618,620,621],{},[412,619,579],{}," is still ahead of the operator's ",[357,622,583],{},[570,624,625,626,629,630],{},"the ",[357,627,628],{},"global watermark"," passes ",[412,631,579],{},[354,633,634],{},"Kafka Streams uses one clock for both decisions; so does Flink. StoatFlow uses Kafka Streams' clock for the first and Flink's for the second. The clocks themselves differ along five axes:",[547,636,637,652],{},[550,638,639],{},[553,640,641,643,646,649],{},[556,642],{},[556,644,645],{},"Kafka Streams stream time",[556,647,648],{},"Flink watermark",[556,650,651],{},"StoatFlow watermark",[565,653,654,674,688,702,725],{},[553,655,656,659,662,669],{},[570,657,658],{},"The quantity",[570,660,661],{},"maximum record timestamp seen",[570,663,664,665,668],{},"per-split generator, then ",[412,666,667],{},"min"," over inputs at each operator",[570,670,671,673],{},[412,672,667],{}," across non-idle partitions − out-of-orderness",[553,675,676,679,682,685],{},[570,677,678],{},"Granularity",[570,680,681],{},"per task",[570,683,684],{},"per operator",[570,686,687],{},"one, application-wide",[553,689,690,693,696,699],{},[570,691,692],{},"Direction of error",[570,694,695],{},"runs ahead of true completeness (optimistic)",[570,697,698],{},"behind it (pessimistic), and different at each operator",[570,700,701],{},"behind it (pessimistic), identical everywhere",[553,703,704,707,714,720],{},[570,705,706],{},"An input that goes quiet",[570,708,709,710,713],{},"holds stream time back; ",[412,711,712],{},"max.task.idle.ms"," bounds the wait",[570,715,716,719],{},[412,717,718],{},"withIdleness"," excludes the split",[570,721,722,724],{},[412,723,718],{}," excludes the partition",[553,726,727,730,733,743],{},[570,728,729],{},"After a restart",[570,731,732],{},"rebuilt from re-read records",[570,734,735,736,742],{},"rebuilt from re-read records — the watermark is not checkpointed (",[377,737,741],{"href":738,"rel":739},"https:\u002F\u002Fissues.apache.org\u002Fjira\u002Fbrowse\u002FFLINK-5601",[740],"nofollow","FLINK-5601",")",[570,744,732],{},[472,746,489],{"id":747},"kafka-streams-one-clock-optimistic-and-per-task",[354,749,750,751,753,754,757,758,761],{},"Kafka Streams' notion of event-time \"now\" is ",[357,752,583],{},": the largest record timestamp a task has processed so far. It lives as a plain field on the windowed processor — ",[412,755,756],{},"observedStreamTime",", advanced with ",[412,759,760],{},"Math.max"," on every record — and since a processor instance is created per task, it is a per-task quantity. A task that owns three partitions has one stream time across them; a different task has another.",[354,763,764,765,768,769,772,773,776,777,780,781,784,785,788],{},"Both decisions hang off it. When a record arrives, ",[412,766,767],{},"KStreamWindowAggregate"," first advances stream time with the record's own timestamp, computes ",[412,770,771],{},"windowCloseTime = observedStreamTime − grace",", and then folds the record into every matching window whose end is still ",[362,774,775],{},"greater than"," that close time; the rest are logged as expired and counted in the ",[412,778,779],{},"dropped-records"," sensor. Emission on window close — ",[412,782,783],{},"EmitStrategy.ON_WINDOW_CLOSE",", or ",[412,786,787],{},"suppress(untilWindowCloses)"," — fires when the same clock passes the same threshold.",[354,790,791,792,795,796,798,799,802],{},"The clock is optimistic: the moment any record with a timestamp from the future arrives, stream time jumps to it, and every window whose close it has now passed stops accepting and emits its final result — even if a sibling partition still holds older records that belong in those windows. Kafka Streams knows this, and the mitigation is ",[357,793,794],{},"timestamp synchronisation",": within a task, the next record to process is always the one with the lowest timestamp across the task's buffered partitions, and ",[412,797,712],{}," (default ",[412,800,801],{},"0",") says how long to wait for a partition whose buffer is empty. At the default the task waits only for data already on the brokers, never for a producer that has yet to send. It is cheap — no coordination, no extra state — and the result is available the instant the last record is processed. The cost is that a single fast partition, or a single record stamped ahead of its neighbours, closes windows early. That is a reasonable trade for a fleet of independent tasks with no channel between them, and it is the model StoatFlow keeps for one of its two decisions.",[472,804,495],{"id":805},"flink-one-clock-pessimistic-and-per-operator",[354,807,808,809,811,812,815,816,819,820,823,824,819,827,823,830,832,833,836],{},"Flink's answer is the ",[357,810,601],{},": a monotonic claim, inserted at the source, that no record with a timestamp below ",[412,813,814],{},"T"," should be expected any more. Each source split runs its own generator — ",[412,817,818],{},"forBoundedOutOfOrderness(d)"," emits ",[412,821,822],{},"maxSeen − d − 1",", ",[412,825,826],{},"forMonotonousTimestamps()",[412,828,829],{},"maxSeen − 1",[412,831,718],{}," lets a quiet split drop out of the calculation — and the watermark travels ",[357,834,835],{},"in-band"," as a special event through the operator graph. Every downstream operator takes the minimum over its input channels and forwards that as its own watermark.",[354,838,839,840,843,844,847,848,470],{},"Both of Flink's decisions run on it. A window fires when the operator's watermark passes the window end, keeps its state for ",[412,841,842],{},"allowedLateness",", re-fires an updated result for each late record that lands inside that span, and drops a record only once the watermark has passed ",[412,845,846],{},"end + allowedLateness",". The clock is pessimistic by construction — it lags the newest data by the out-of-orderness bound, and by the slowest input — which is exactly what makes a final result ",[362,849,850],{},"final",[354,852,853,854,858,859,864,865,868,869,872,873,876],{},"The price is that \"the watermark\" is not one number. Two operators in the same job hold different watermarks at the same instant, because they sit at different depths and behind different inputs, so they can disagree about whether the same record is late. That per-operator divergence is the fourth of the failure modes the ",[377,855,857],{"href":856},"\u002Fblog\u002Finternal-consistency#why-stoatflow-is-different","internal-consistency post"," discusses, and it is the reason Flink needs machinery around the clock: ",[377,860,863],{"href":861,"rel":862},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FFLINK\u002FFLIP-217%3A+Support+watermark+alignment+of+source+splits",[740],"FLIP-217"," alignment pauses a split that has run too far ahead of its group. That is a consequence of distribution, not of watermarks. One thing Flink does ",[362,866,867],{},"not"," do is checkpoint the watermark — timers survive a restore, the watermark does not (",[377,870,741],{"href":738,"rel":871},[740],", open since 2017) — so after a restore every operator's watermark restarts at ",[412,874,875],{},"Long.MIN_VALUE"," and is re-derived from what the sources emit next. StoatFlow re-derives its watermark the same way (part 3).",[472,878,501],{"id":879},"stoatflow-both-clocks",[354,881,882,883,885,886,823,889,892,893,896,897,900,901,903,904,907,908,910,911,914,915,918],{},"StoatFlow's public API is Flink's, deliberately. A source declares its time semantics with a ",[412,884,428],{}," — ",[412,887,888],{},"forBoundedOutOfOrderness",[412,890,891],{},"forMonotonousTimestamps"," or ",[412,894,895],{},"noWatermarks",", plus ",[412,898,899],{},"withTimestampAssigner"," and ",[412,902,718],{}," — attached per source through ",[412,905,906],{},"Consumed.withWatermarkStrategy",". A source that declares nothing gets ",[412,909,888],{}," with the application-wide ",[412,912,913],{},"stoatflow.watermark.max-out-of-orderness-ms",", 10 seconds by default. The ",[412,916,917],{},"withWatermarkAlignment"," decorator from FLIP-217 exists too, signature-identical, for code written against Flink.",[920,921,926],"pre",{"className":922,"code":923,"language":924,"meta":925,"style":925},"language-kotlin shiki shiki-themes vitesse-light","val orders = builder.stream(\n    \"orders\",\n    Consumed.with(Serdes.String(), orderSerde)\n        .withWatermarkStrategy(\n            WatermarkStrategy.forBoundedOutOfOrderness\u003CString, Order>(Duration.ofSeconds(30))\n                .withTimestampAssigner { _, order, _ -> order.placedAt }\n                .withIdleness(Duration.ofMinutes(2)),\n        ),\n)\n","kotlin","",[412,927,928,955,965,983,994,1029,1046,1067,1073],{"__ignoreMap":925},[929,930,933,937,941,945,948,952],"span",{"class":931,"line":932},"line",1,[929,934,936],{"class":935},"sbBg2","val",[929,938,940],{"class":939},"suHK_"," orders ",[929,942,944],{"class":943},"sYZai","=",[929,946,947],{"class":939}," builder.",[929,949,951],{"class":950},"sySUi","stream",[929,953,954],{"class":939},"(\n",[929,956,958,962],{"class":931,"line":957},2,[929,959,961],{"class":960},"spphp","    \"orders\"",[929,963,964],{"class":939},",\n",[929,966,968,971,974,977,980],{"class":931,"line":967},3,[929,969,970],{"class":939},"    Consumed.",[929,972,973],{"class":950},"with",[929,975,976],{"class":939},"(Serdes.",[929,978,979],{"class":950},"String",[929,981,982],{"class":939},"(), orderSerde)\n",[929,984,986,989,992],{"class":931,"line":985},4,[929,987,988],{"class":939},"        .",[929,990,991],{"class":950},"withWatermarkStrategy",[929,993,954],{"class":939},[929,995,997,1000,1002,1005,1008,1010,1013,1016,1019,1022,1026],{"class":931,"line":996},5,[929,998,999],{"class":939},"            WatermarkStrategy.",[929,1001,888],{"class":950},[929,1003,1004],{"class":939},"\u003C",[929,1006,979],{"class":1007},"sUxyF",[929,1009,823],{"class":939},[929,1011,1012],{"class":1007},"Order",[929,1014,1015],{"class":939},">(Duration.",[929,1017,1018],{"class":950},"ofSeconds",[929,1020,1021],{"class":939},"(",[929,1023,1025],{"class":1024},"s-TwI","30",[929,1027,1028],{"class":939},"))\n",[929,1030,1032,1035,1037,1040,1043],{"class":931,"line":1031},6,[929,1033,1034],{"class":939},"                .",[929,1036,899],{"class":950},[929,1038,1039],{"class":939}," { _, order, _ ",[929,1041,1042],{"class":943},"->",[929,1044,1045],{"class":939}," order.placedAt }\n",[929,1047,1049,1051,1053,1056,1059,1061,1064],{"class":931,"line":1048},7,[929,1050,1034],{"class":939},[929,1052,718],{"class":950},[929,1054,1055],{"class":939},"(Duration.",[929,1057,1058],{"class":950},"ofMinutes",[929,1060,1021],{"class":939},[929,1062,1063],{"class":1024},"2",[929,1065,1066],{"class":939},")),\n",[929,1068,1070],{"class":931,"line":1069},8,[929,1071,1072],{"class":939},"        ),\n",[929,1074,1076],{"class":931,"line":1075},9,[929,1077,1078],{"class":939},")\n",[354,1080,1081,1082,1085],{},"What happens behind that API is where StoatFlow departs from both engines. One generator runs per source ",[362,1083,1084],{},"partition",", and because the whole topology is one process, there is no operator graph to propagate through and no shuffle to cross. Each partition's generator advances as the dispatcher — the single thread that hands records to the key-affinity lanes — dispatches its records, and the tracker takes the minimum across all partitions that are not idle and stores one value: on the consumer thread every 200 ms, and once more just before every commit barrier. That value is delivered to every sub-topology as the same event, in the same epoch. Two operators cannot disagree about the watermark, because there is only one; there is no coordination protocol to configure, because there is nothing to coordinate. This is the property the internal-consistency result rests on, and it comes free with the single-instance architecture rather than being engineered on top of it.",[354,1087,1088,1089,1091,1092,1094,1095,1097,1098,1101],{},"That watermark is the ",[357,1090,537],{}," clock. ",[412,1093,544],{}," emissions, ",[412,1096,787],{},", the null side of a left or outer stream-stream join, event-time timers and ",[412,1099,1100],{},"STREAM_TIME"," punctuators all fire on it.",[354,1103,1104,1105,1107,1108,1111,1112,1115,1116,1119,1120,1123,1124,892,1127,1130,1131,1133,1134,1137],{},"The ",[357,1106,457],{}," clock is a different value. Every windowed aggregation in StoatFlow also keeps a stream time of its own — the highest event time that operator has processed, an ",[412,1109,1110],{},"AtomicLong"," because records reach it from many lanes at once — and the late-drop rule reads that, not the watermark: a record is dropped when its window's ",[412,1113,1114],{},"end + grace"," is already ",[362,1117,1118],{},"behind"," the operator's stream time, and folded in otherwise. Session windows use Kafka Streams' post-merge predicate, so a late record that bridges two still-open sessions is rescued exactly as it would be there. Drops are counted in ",[412,1121,1122],{},"stoatflow.dropped.records.total"," with ",[412,1125,1126],{},"reason=\"late_window\"",[412,1128,1129],{},"\"late_session\"",", the counterpart of Kafka Streams' ",[412,1132,779],{},", and never sent to a dead-letter topic. Why not judge acceptance on the watermark too? Because it is pessimistic by the bound and by the slowest partition: every window would keep admitting records for that long after Kafka Streams had sealed them. The watermark's job is to make the ",[362,1135,1136],{},"final result"," safe, not to widen the window.",[472,1139,507],{"id":1140},"grace-the-same-rule-the-close-is-what-differs",[354,1142,1143,1144,1147],{},"Our Kafka Streams compatibility matrix carried a row for grace that read: ",[362,1145,1146],{},"Kafka Streams — retention after close; StoatFlow — extends acceptance window."," ADR-016, the decision record behind StoatFlow's window lifecycle, said the same thing in more words: Kafka Streams' grace \"extends state retention, not event acceptance\", and late records \"are silently dropped unless using suppress\". It needs correcting, twice.",[354,1149,1150,1153,1154,1159,1160,1163,1164,1167,1168,1171],{},[357,1151,1152],{},"First correction: Kafka Streams grace bounds acceptance."," The retention description was true before ",[377,1155,1158],{"href":1156,"rel":1157},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-328%3A+Ability+to+suppress+updates+for+KTables",[740],"KIP-328"," landed in 2.1. Since then ",[412,1161,1162],{},"TimeWindows.ofSizeAndGrace(size, grace)"," is an acceptance bound in exactly the sense the ADR claimed for StoatFlow: a record is folded in while ",[412,1165,1166],{},"windowEnd > observedStreamTime − grace"," and dropped — logged, and metered — once it is not. Retention is a separate, storage-level knob (",[412,1169,1170],{},"Materialized.withRetention",", which must be at least size plus grace) on both engines.",[354,1173,1174,1177,1178,1180,1181,1184],{},[357,1175,1176],{},"Second correction: StoatFlow's acceptance rule is the same one."," When we fixed the row after finding the first problem, we wrote that StoatFlow measures acceptance against the global watermark. It does not — the section above is the code. Both engines admit a record while ",[412,1179,579],{}," is ahead of the operator's max-observed stream time; StoatFlow's comparison is strict where Kafka Streams' is inclusive, so a record whose window closes exactly ",[362,1182,1183],{},"at"," stream time is kept for one more tick, and that is the entire divergence on the way in. What the watermark governs is the way out: the moment the result is final.",[354,1186,1187],{},"The diagram makes the split concrete. One window from 10:00 to 10:05 with a minute of grace, so it closes at 10:06; two source partitions; a 30-second out-of-orderness bound; nine records in the order they reach the operator.",[354,1189,1190],{},[1191,1192],"img",{"alt":1193,"src":1194},"Nine records arrive in order across two partitions, P0 and P1. Stream time, the highest event time seen, passes the 10:06 close at arrival seven. The watermark, the per-partition minimum less 30 seconds, passes it only at arrival nine, because P1 lags. Record eight, event time 10:04:45, arrives between the two: Kafka Streams and StoatFlow both drop it, Flink keeps it. Kafka Streams emits a final count of 5 at arrival seven, StoatFlow a final count of 5 at arrival nine, Flink a final count of 6 at arrival nine.","\u002Fassets\u002Fblog\u002Fthree-clocks_accept-vs-close.svg",[354,1196,1197,1198,1201],{},"Read the two clock rows first; each shows the clock's value ",[362,1199,1200],{},"after"," that arrival has been folded in. Stream time — the highest event time seen — passes the 10:06 close at arrival ⑦, a record from the next window stamped 10:06:40. The watermark passes it only at arrival ⑨, because P1 is behind and the minimum follows the partition that is behind. Record ⑧, stamped 10:04:45 and squarely inside the window, arrives between those two moments.",[354,1203,1204,1205,1209],{},"Now the three verdict rows. Kafka Streams drops ⑧ — stream time is 10:06:40, the window's close was 10:06:00 — and has already emitted its final count of 5 at ⑦. StoatFlow drops ⑧ for the same reason, on the same clock, and emits the same final count of 5 — at ⑨, when the watermark passes the close. Flink's acceptance clock is the watermark, which reads 10:04:00 when ⑧ arrives (10:04:15 once it is counted), so Flink keeps it and emits 6 at ⑨. None of the three is wrong; StoatFlow keeps the Kafka Streams admission rule so a port behaves identically on the way in, and takes the later close so that \"final\" means every contributing record is behind one application-wide claim. The ",[377,1206,1208],{"href":1207},"\u002Fdocs\u002Fconcepts\u002Fevent-time-and-watermarks#window-closing","concept page's timeline"," walks the single-partition lifecycle of the same window.",[472,1211,513],{"id":1212},"what-a-ported-application-sees",[354,1214,1215,1216,1218],{},"A Kafka Streams application ported to StoatFlow keeps its window definitions, its grace periods and its ",[412,1217,414],{}," calls, and its windows admit the same records. Four things change.",[354,1220,1221,1226,1227,1230,1231,1234,1235,1238,1239,1242,1243,1245,1246,1249,1250,1253],{},[357,1222,1223,1225],{},[412,1224,418],{}," returns the watermark."," In Kafka Streams, ",[412,1228,1229],{},"ProcessorContext.currentStreamTimeMs()"," is the task's max-observed timestamp. In StoatFlow it delegates to ",[412,1232,1233],{},"currentWatermarkMs()",", the native name for the global watermark — the minimum across non-idle partitions, minus the out-of-orderness bound. Two consequences follow: the value is generally ",[357,1236,1237],{},"lower"," than Kafka Streams stream time, and it is ",[357,1240,1241],{},"global"," rather than per-task. Logic that compares a record's timestamp to \"now\" sees a larger gap; a deadline keyed on stream time fires later. Before the first record has been processed the watermark is ",[412,1244,875],{},", and ",[412,1247,1248],{},"isLate()"," is defined against it. The ",[377,1251,1252],{"href":275},"porting guide"," flags this divergence first.",[354,1255,1256,1259,1260,1262,1263,1265],{},[357,1257,1258],{},"Final results arrive later."," An ",[412,1261,544],{}," emission, a ",[412,1264,787],{}," release, or the null side of a left join appears when the watermark passes the close — later by at least the out-of-orderness bound, plus up to two commit intervals under exactly-once (part 3). Results emitted on every update, the default, are unaffected.",[354,1267,1268,1273],{},[357,1269,1270,1272],{},[412,1271,1100],{}," punctuators fire on the watermark."," Kafka Streams fires them on stream-time advance; StoatFlow on watermark advance. A punctuator that expected to run the instant a future-stamped record arrived now runs when the whole application has caught up to it.",[354,1275,1276,464,1279,1282,1283,1286,1287,1289],{},[357,1277,1278],{},"One source can opt back into stream time.",[412,1280,1281],{},"Consumed.withTimestampExtractor(extractor)"," adapts a Kafka Streams ",[412,1284,1285],{},"TimestampExtractor"," onto a per-source strategy with a monotonous generator, so that source contributes Kafka Streams stream time to the watermark and its windows close as they did before the port. It shares the single per-source strategy slot with ",[412,1288,991],{},"; part 2 has the details, and the bug it shipped with.",[472,1291,519],{"id":1292},"tradeoffs-and-limits",[354,1294,1295,1298],{},[357,1296,1297],{},"The close is conservative, always."," Every window's final result is later than under Kafka Streams by at least the out-of-orderness bound, because the watermark trails the newest data by that much before the slowest partition is even considered. The 10-second default is a default, not a recommendation: size it to the disorder the data actually has, or the latency is paid for nothing.",[354,1300,1301,1304],{},[357,1302,1303],{},"One lagging, non-idle partition delays every window."," The minimum has no locality. A partition that keeps delivering but delivers old timestamps holds the watermark for the whole application — every window, every timer, every suppress buffer — until it catches up or goes quiet long enough to be excluded. Part 2 is the configuration discipline that follows from that.",[354,1306,1307,1310,1311,1314,1315,1318],{},[357,1308,1309],{},"Two one-tick divergences at exact boundaries."," The late-drop comparison is strict where Kafka Streams' is inclusive, so a record whose window closes exactly at stream time is kept one tick longer; and a left or outer stream-stream join opens its null side when the watermark ",[362,1312,1313],{},"reaches"," the close, where Kafka Streams waits for it to pass. Both are recorded in ADR-118 and both are deliberate — the first survived a review that proposed the inclusive form, because the inclusive form drops a boundary record that the exactly-once close (part 3) is designed to include. Output ",[362,1316,1317],{},"sets"," are identical; the boundary tick is not.",[354,1320,1321,1326],{},[357,1322,1323,1325],{},[412,1324,418],{}," is an alias."," It is kept so ported code compiles, not because it returns the Kafka Streams quantity; there is no per-task stream time to ask for.",[354,1328,1329,1332],{},[357,1330,1331],{},"Ingestion time is not supported."," Event time from the record or an assigner, or processing time; not the broker append time as a third notion.",[354,1334,1335,1338],{},[357,1336,1337],{},"DIRECT dispatch has no watermarks."," The processing-time fast path skips watermark tracking, and the engine refuses to combine it with an event-time topology; nothing in this post applies to it.",[472,1340,525],{"id":1341},"whats-next",[354,1343,1344,1345,1348,1349,1352],{},"The minimum is the sharp edge of this design. Everything above assumes the watermark moves — and a single partition that stops advancing, or a single source configured with ",[412,1346,1347],{},"noWatermarks()",", holds every window in the application still. Part 2, ",[362,1350,1351],{},"Making the watermark move",", is the discipline that follows: what each source has to say about its own time, what idleness actually does and the bug that made it necessary, why one strategy stalls a whole topology, and why StoatFlow's ordered dispatch already does most of what Flink needs watermark alignment for.",[354,1354,1355],{},[357,1356,1357],{},"Read on:",[396,1359,1360,1365,1370,1377,1384,1394,1401],{},[399,1361,1362,1364],{},[377,1363,72],{"href":73}," — the concept page this post extends.",[399,1366,1367,1369],{},[377,1368,321],{"href":322}," — the corrected time-semantics rows.",[399,1371,1372,1376],{},[377,1373,1375],{"href":1374},"\u002Fblog\u002Finternal-consistency","Internal consistency"," — what one application-wide watermark makes possible.",[399,1378,1379,1383],{},[377,1380,1382],{"href":1156,"rel":1381},[740],"KIP-328: Ability to suppress updates for KTables"," — where Kafka Streams grace became an acceptance bound.",[399,1385,1386,885,1391,1393],{},[377,1387,1390],{"href":1388,"rel":1389},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-695%3A+Further+improve+Kafka+Streams+timestamp+synchronization",[740],"KIP-695: Further improve Kafka Streams timestamp synchronization",[412,1392,712],{},", and why an optimistic clock needs it.",[399,1395,1396,1400],{},[377,1397,1399],{"href":861,"rel":1398},[740],"FLIP-217: Support watermark alignment of source splits"," — Flink's answer to per-operator drift.",[399,1402,1403,1407],{},[377,1404,1406],{"href":738,"rel":1405},[740],"FLINK-5601: Window operator does not checkpoint watermarks"," — why a Flink watermark, like StoatFlow's, is re-derived after a restore.",[1409,1410,1411],"style",{},"html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}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);}",{"title":925,"searchDepth":957,"depth":957,"links":1413},[1414,1415,1416,1417,1418,1419,1420,1421,1422],{"id":474,"depth":957,"text":475},{"id":528,"depth":957,"text":483},{"id":747,"depth":957,"text":489},{"id":805,"depth":957,"text":495},{"id":879,"depth":957,"text":501},{"id":1140,"depth":957,"text":507},{"id":1212,"depth":957,"text":513},{"id":1292,"depth":957,"text":519},{"id":1341,"depth":957,"text":525},"2026-09-15","Kafka Streams, Flink and StoatFlow answer \"is this window done?\" with three different clocks. StoatFlow runs two of them: it accepts a late record the way Kafka Streams does, on the operator's stream time, and closes the window the way Flink does, on a single global watermark. Part 1 of a four-part series on event time — including two corrections to a claim our own compatibility matrix made about grace.","md",{"src":1427},"\u002Fassets\u002Fblog\u002Fog\u002Fthree-clocks-stream-time-vs-watermarks.png",{},true,null,"\u002Fblog\u002Fthree-clocks-stream-time-vs-watermarks",{"title":336,"description":1424},{"loc":1431},"3.blog\u002F15.three-clocks-stream-time-vs-watermarks","cL1OKXXsuuR8SZnY84ZO5ww61IAPtgOyXNNvB54e3AA",[1437,1442],{"title":1438,"path":1439,"stem":1440,"description":1441,"children":-1},"Kafka Streams 4.4.0: transactional state stores, headers-aware partitioning and queries","\u002Fblog\u002Fkafka-streams-4-4","3.blog\u002F20.kafka-streams-4-4","What Apache Kafka 4.4.0 brings to Kafka Streams: opt-in transactional state stores (KIP-892), headers in the partitioner and IQv2, the GlobalKTable joiner fix, and static membership and plugins for the Streams Rebalance Protocol — plus where StoatFlow stands.",{"title":1443,"path":1444,"stem":1445,"description":1446,"children":-1},"Scheduled sources: a topology source that isn't a topic","\u002Fblog\u002Fscheduled-sources","3.blog\u002F14.scheduled-sources","Every source in a Kafka Streams topology is a topic. The calendar and your own state — a snapshot across keys, a sweep over what you hold — needed a job outside the topology or a punctuator bent into a source. StoatFlow's scheduled sources run your code on a schedule and turn what it forwards into an ordinary KStream: keyed, processed in parallel on the lanes, committed with the stream. What that opens up, how it works, two verified examples, and the catch — the emitter's view is read-only, a run does not yield to live traffic on its own, and a read from outside the engine is not in the transaction.",1791929252107]