How Flink’s Kafka connector commits offsets to Kafka

October 24th, 2026

An unnecessarily deep dive into how Flink commits Kafka consumer offsets.

A pull request of mine was merged into flink-connector-kafka this week. A ridiculous number of sketches and diagrams went into the commits to get the idea straight in my head, so in this post I tidied them up a bit to share.

How consumer offsets are used by Flink

If you run a Flink job that reads from Kafka, you’ve probably seen its consumer group in your Kafka tooling like any other Kafka consumer: committed offsets for each partition, and a lag figure showing how far behind the log end the job is.

Flink doesn’t need to depend on those committed offsets for its own work. The unit of work for a Flink source is a “split”. For Kafka, a split is a partition and the offset to read from (and for a bounded split, the offset to stop reading at). This is stored in Flink state. Each source reader holds a split for every partition it reads, and each split records the next Kafka offset to read.

When Flink takes a checkpoint, each split is saved in the checkpoint, and contains those offsets. If the job fails, or you restart it from a savepoint, the source picks up from the offsets in the checkpoint. That’s how Flink can offer exactly-once processing: the offsets and the rest of the job’s state are captured together, in the same consistent snapshot.

Typically, the Kafka source reader commits the checkpointed offsets to Kafka once a checkpoint has completed. Although that is not necessary for the Flink job, it’s useful for lag dashboards and alerting, in the same way as any other Kafka consumer, and helps to understand how the job is doing.

Lag that wasn’t going away

FLINK-33484 reported a problem with a Flink job consuming a transactional topic with isolation.level=read_committed. The job had read everything on the topic, but the reported lag according to Kafka was stuck at 1, not 0.

A plain Kafka Java consumer reading the same data from the same topic reported no lag. Flink wasn’t committing the right offset.

Every time a record is handed on to the rest of the job, the split’s position was updated:

splitState.setCurrentOffset(consumerRecord.offset() + 1);

The next offset to read is the one after the last record.

That’s the right thing to do for a non-transactional, uncompacted topic, where every offset holds a record.

It’s not always the right thing to do.

When a Kafka producer commits or aborts a transaction, the broker writes a transaction marker into each partition the transaction touched. That marker takes up an offset in the log like any other entry.

This is handled by the Kafka consumer. Transaction markers aren’t given to the application.

And if the application only asks for committed records (read_committed), records from aborted transactions are skipped in the same way. The consumer reads past them, and its position moves forward, but no record comes out for them.

Imagine a Kafka producer writes a single record in a transaction and commits it:

offset 0 record delivered to Flink
offset 1 commit marker read by the consumer
not delivered to Flink

The topic partition’s log end offset (and last stable offset) is 2 because that’s where the next entry will be written.

Flink emitted the record at offset 0, and set the split’s current offset to 1 (because it added 1 to the offset it’s emitted).

The Kafka consumer has actually read past the transaction marker, so its position is 2.

But at the next checkpoint, Flink committed an offset of 1.

Any Kafka tooling set up to monitor Kafka consumer progress compared that with a log end of 2, and reported a lag of 1.

On a busy topic, you’d barely notice. The next record arrives, gets emitted, and the committed offset jumps over the marker. The committed offset was always slightly behind, but only until new data arrives.

On a quiet or bursty topic, “until new data arrives” can be a long time. The job was fully caught up, but every checkpoint committed the same stale offset, and Kafka monitoring reported a lag that didn’t exist.

That was FLINK-33484 that I picked up a little while ago.

Read the rest of this entry »

Adding support for remote jar fetching in Flink session clusters

August 23rd, 2026

I’ve submitted a Flink Improvement Proposal (FLIP) about adding support for Flink to be able to fetch remote jars for applications submitted to a session cluster.

Discussion thread on lists.apache.org

Read the rest of this entry »

Why we can’t have nice things

August 9th, 2026

Looking at the server request logs for Machine Learning for Kids is a depressing reminder of what it means to run something on the Internet nowadays.

I was skimming through the request logs for the Machine Learning for Kids servers yesterday [1], and what jumped out at me was the constant rate of requests to non-existent paths that I just generate HTTP 404 responses for.

These aren’t someone accidentally mistyping a URL.

I grepped an excerpt from the log for requests that got an HTTP-404 response and put the result in a gist.

Read the rest of this entry »

Importing and exporting projects in Machine Learning for Kids

August 8th, 2026

I’ve added new features to Machine Learning for Kids to give students more options about where they store their projects.

Limits

In Machine Learning for Kids, students create projects where they collect training examples to train their own custom machine learning models. I don’t limit the number of projects students can create. I want students to create as many projects as they’d like. But for projects stored on my server, I’ve nudged students to delete projects once they are no longer working on them. This is intentional, as a way of helping me keep the running costs for the site under control.

Read the rest of this entry »

Running Flink jobs in MiniCluster using the Kubernetes Operator

August 5th, 2026

I’ve submitted a Flink Improvement Proposal (FLIP) about adding support for the Flink Kubernetes Operator to run Flink’s MiniCluster in a single pod for low-throughput jobs that require isolation.

In the last few weeks, I’ve been working on a proof-of-concept to demonstrate the feasibility of this idea. I’ve done enough to convince myself that this is viable and identify where the issues will be, but I’m looking for community feedback before I take it much further.

Background

The Flink Kubernetes Operator is one of the best ways to run Flink jobs. From the documentation:

Flink deployments are declared like any other Kubernetes workload, and the operator runs their whole operational life:

  • Lifecycle Management: deployment, stateful upgrades, rollbacks, and self-healing
  • Zero-Downtime Upgrades: blue/green deployments that switch over only once the new version is proven healthy
  • Autoscaling: parallelism and memory continuously right-sized to the observed load
  • Kubernetes-Native Operations: Helm installation, RBAC, high availability, metrics, logging, and ingress

You create a FlinkDeployment Kubernetes custom resource that points to your Flink application, and the Operator handles provisioning independently schedulable and independently configurable Job Manager Deployment and Task Manager Deployment, configuring them to form a distributed Flink cluster.

This provides scalability and high availability, and is the right approach in a lot of situations. However, this comes at the cost of a fixed baseline cost of at least one JobManager pod and one or more TaskManager pods.

A smaller, lighter-weight alternative would be useful for small or intermittent jobs, where the minimum resource cost of two separately-scheduled pods is disproportionate to the job itself.

A single-pod, self-contained Flink job that starts fast and needs no multi-pod coordination could be a good fit for low-throughput jobs that aren’t suitable for session clusters because they need isolation.

Read the rest of this entry »

Generative AI with tool calling in Scratch

July 10th, 2026

In this post, I want to share a new feature in the generative AI support in Machine Learning for Kids: tool calling.

I wrote last year about how I introduced Generative AI in Machine Learning for Kids by adding support for projects using small language models. And earlier this year, I walked through my six favourite projects for explaining different aspects of Gen AI.

This week I’ve been working on extending the language model support in the site, by adding a new model that is capable of tool calling.


demo video at youtu.be/HdcTseNvjhU

Why is tool calling useful?

Try asking a model what the weather is like in New York right now.

If the temperature and top-p is high enough, many of the models will likely hallucinate an answer.

If the temperature and top-p is low enough, the models can just respond that they cannot answer that.

(If you don’t know why “temperature” makes that difference, I’ve written about that before and have a student worksheet that focuses on this.)

This second answer is more accurate at least, but it is still not helpful. The point is, if you ask the model for something that can’t possibly be represented in the knowledge used to create it, the model cannot give a helpful answer by itself.

Tool calling helps in these situations by making tools available to the model. It can call these tools to help respond to prompts that can’t be answered by the model alone.

Read the rest of this entry »

Why I enjoy my commute

July 5th, 2026

My commute to work is one of the favourite parts of my day. I’ve mentioned it before, more often than I probably should, but in this post I try and explain why I enjoy it so much.

The journey

Otterbourne village and crossing the motorway

I start off walking through Otterbourne: past the pub, across Otterbourne Hill Green and past the weird mini-castle at Otterbourne Hill Reservoir (I still don’t really understand what that’s all about).

Read the rest of this entry »

I have a new job!

July 4th, 2026

In April, I wrote an outline of my career. That post is already out of date, because I have a new job to add to the list.

I’m joining Confluent as a Principal Software Engineer working on Confluent Platform for Apache Flink.

I won’t try and describe what “Confluent Platform for Flink” is here. Partly because I’m sure I’ll have plenty to say about it in coming months. Mostly because I’ve got a lot to learn and anything I say today will almost certainly be incomplete or inaccurate in ways I’ll find embarrassing once I know more. In the meantime, the product page or the docs site are the best places to go if you’re curious.

But that won’t stop me speculating about what this all means for me.

Read the rest of this entry »