Interactive Queries in Kafka

Interactive Queries in Kafka

Kafka is superb at moving events, but sooner or later somebody asks, "What is the current balance of customer 42?" This guide explains how a Kafka Streams application can answer such questions directly from its own state, without a separate database, and what you must build around it to make it work.

A product manager walks over to your desk and asks a simple question: “Can we show each customer their running order total on the website, updated live?” You already have the data. Every order is an event in a Kafka topic, and a stream-processing application already adds them up. The totals exist, in memory and on disk, inside your application. The only problem is that nobody outside the application can see them.

The usual answer is to copy the totals into a database and query that. It works, but it means another system to run, secure, keep in sync and pay for. Kafka Streams offers a different answer called Interactive Queries: let other programs ask the application itself. The application already holds the totals, so why not just answer?

This guide explains how that works from the ground up. You will learn where the data lives, why a single instance only knows part of the answer, how to find the instance that knows the rest, what happens during restarts and rebalances, how fresh the answers really are, and how to design, secure, monitor and test the whole thing. Every new word is explained when it first appears, and each big idea comes with an everyday comparison and worked numbers.

KKafka in Five Minutes

A short refresher, so that the rest of the guide stands on firm ground.

Apache Kafka moves streams of events between programs. A program that writes events is a producer, and a program that reads them is a consumer. The servers in the middle are brokers. Events are organised into named streams called topics, and each topic is split into partitions: ordered lists of messages that can live on different brokers so the work can be shared.

Each message has a key and a value. The key decides the partition: messages with the same key always go to the same partition, and inside a partition the order is preserved. This detail is the foundation of everything in this guide. It means that all the events for customer 42 sit together, in order, in one partition.

Everyday analogy

Imagine a city with several neighbourhood libraries. Each library holds certain shelves of the city’s records, and every record has a fixed shelf based on the first letters of the customer’s name. Whoever needs customer 42’s record knows exactly which shelf, and therefore which library, holds it. We will keep using this city of libraries throughout the guide.

Stream

Topic

A named channel of events, such as orders or payments.

Shelf

Partition

One ordered slice of a topic. All messages with the same key land in the same partition.

Label

Key

The field that decides the partition, for example a customer id.

Server

Broker

A Kafka server that stores partitions and serves requests.

i
Keep this in mind

Because a key always maps to one partition, the question “who holds customer 42’s data?” always has exactly one answer at any moment. Interactive Queries rely on this fact.

PThe Problem: Kafka Stores Events, Not Answers

A Kafka topic is a log. It is wonderful for recording what happened, and awkward for asking what is true now.

Suppose a topic holds every order ever placed. To learn the total for customer 42, you would have to read all of that customer’s orders from the start and add them up. There is no way to say “give me the latest value for key 42” and get an instant reply. A topic has no index on keys, and it is not designed for lookups. Reading it again and again for each question would be slow and wasteful.

QuestionEasy on a topic?Why
What happened, in order?YesThat is exactly what a log records
What is the latest event for key 42?NoYou must scan back from the end of one partition
What is the running total for key 42?NoYou must read and add every event for that key
Which keys have a total above 1,000?NoYou must aggregate everything first

The usual fix, and its cost

Teams typically add a stream-processing job that keeps running totals, and then write those totals into an external database such as a relational or key-value store. Applications query that database. This is a proven design, but it has costs: one more system to operate, one more copy of the data, one more place where the copy can be wrong or late, and one more dependency in the request path.

Copying results to a database

  • Familiar tools, query languages and skills
  • Rich queries, joins and indexes
  • Independent scaling of reads
  • Works with any client technology

What it adds

  • Another system to run, secure and monitor
  • A second copy that can drift from the first
  • Extra delay between the event and the answer
  • Extra failure modes in the write path
i
The idea behind Interactive Queries

The stream-processing job already holds the answer in its own state. If that state can be read directly, the extra database may not be needed for many use cases.

SKafka Streams in Five Minutes

Interactive Queries are a feature of Kafka Streams, so we need a quick tour of that library first.

Kafka Streams is a library, not a separate cluster. You add it to an ordinary application, describe how events should be transformed, and run as many copies of the application as you like. Each copy is called an instance. Together, the instances share the work automatically, using the same group mechanism as consumers.

The description of the processing is called a topology: a graph of steps such as reading from a topic, filtering, grouping by key, counting, joining, and writing to another topic. Two kinds of data structures appear in it. A stream (KStream) is an endless sequence of events. A table (KTable) is the latest value per key, like a spreadsheet that keeps updating. Aggregations, such as a running total per customer, turn a stream into a table.

Tasks: the unit of parallel work

Kafka Streams divides the work into tasks. Each task processes one partition (or one aligned set of partitions) of the input topics. If the input topic has 12 partitions, the application has 12 tasks, and these tasks are spread across the running instances. With three instances, each typically runs four tasks. Add a fourth instance and Kafka Streams moves some tasks to it.

12
partitions in the input topic
12
tasks, one per partition
4 each
with three instances sharing the tasks
Everyday analogy

Each library branch (instance) looks after a few shelves (tasks). Every shelf has its own librarian who files new arrivals and keeps a running index of what is on it. If a new branch opens, some shelves are moved there.

i
Key fact for later

An aggregation task only ever sees the events of its own partition, so it holds the totals only for the keys in that partition. No single task, and no single instance, knows the totals of everyone.

TState Stores: Local Memory With a Backup

To keep running totals, a task needs somewhere to remember them. That place is called a state store.

A state store is a small database that lives inside the application instance, next to the task that uses it. By default it is backed by RocksDB, a fast embedded key-value store that keeps recent data in memory and the rest on local disk. Reads and writes are local and quick, with no network call, which is why stateful stream processing can be so fast.

Local disks fail, and instances get replaced. To survive this, every write to a state store is also recorded in a special Kafka topic called a changelog topic. If an instance is lost, its replacement reads the changelog and rebuilds the store. The changelog is the backup, and Kafka’s replication keeps that backup safe.

flowchart LR
  IN["Input topic partition"] --> TASK["Stream task"]
  TASK --> STORE["Local state store
RocksDB"] STORE -->|"every update"| CL["Changelog topic in Kafka"] CL -->|"used to rebuild after a failure"| STORE TASK --> OUT["Output topic"]
Figure 1: a task reads from its input partition, keeps state in a local store, and backs every update up to a changelog topic.
Everyday analogy

Each branch keeps a card index on its own shelves for quick lookups. Every time a card is changed, a copy of the change is sent to head office. If the branch burns down, a new branch is set up and head office sends the changes back, so the index is rebuilt card by card.

Restoring takes time

Rebuilding a store means replaying its changelog. For a small store this is seconds. For a store with hundreds of millions of keys, it can take many minutes or even hours. During that time the store is not ready to answer questions. This matters greatly for Interactive Queries, since a query cannot be answered from a store that is still being rebuilt.

PartWhere it livesPurpose
State storeLocal disk and memory of an instanceFast reads and writes for processing
Changelog topicKafka brokers, replicatedDurable backup for rebuilding the store
Record cacheMemory of an instanceBatches updates to reduce writes and downstream traffic
Standby replica (optional)Another instance’s diskA warm copy that speeds up failover

WWhat Are Interactive Queries?

Interactive Queries let code outside the stream-processing flow read the state stores that the flow maintains.

Interactive Queries, sometimes called queryable state, are the part of the Kafka Streams library that gives your own code read-only access to the state stores of a running application. Your code might be a web service handler, a command-line tool or a dashboard backend. Instead of asking a database for customer 42’s total, it asks the Streams application, which reads it from its local store.

It is

A read-only window into state

You can look up a key, scan a range of keys, or read windowed values. You cannot change the state through this door.

It is

Part of the Streams library

It runs inside your application process. There is no separate query server to install.

It is not

A query feature of Kafka itself

Brokers and topics cannot be queried this way. Only state built by a Kafka Streams application can.

It is not

A complete server

The library gives you the lookup and the discovery. You must supply the network layer that lets others ask.

The two halves of an answer

Because no single instance holds all the state, answering a question takes two steps. If the key you want is in this instance’s own stores, you read it locally. If it is held by a different instance, you must find that instance and ask it. Kafka Streams helps with the first half completely and with the discovery part of the second. The actual conversation between instances is yours to build.

Everyday analogy

A visitor asks the front desk of one branch for a record. The desk checks its own shelves first. If the record is not there, the desk consults the city catalogue to find which branch holds it, and telephones that branch. The library system provides the catalogue but not the telephones.

!
Not a general-purpose database

Interactive Queries are best for looking up results that a stream job already computes. They are not designed for ad hoc queries across arbitrary fields, joins the job never made, or heavy analytics. We will compare the options in a later chapter.

BThe Big Picture: Where the Data Lives and Who Can Answer

Here is the whole system in one picture, followed by a walk through each part.

flowchart TD
  CL["Client, for example a web page"] --> LB["Load balancer"]
  LB --> I1["Instance A
holds partitions 0 to 3"] LB --> I2["Instance B
holds partitions 4 to 7"] LB --> I3["Instance C
holds partitions 8 to 11"] I1 |"forward when the key lives elsewhere"| I2 I2 |"forward when the key lives elsewhere"| I3 I1 |"forward when the key lives elsewhere"| I3 K["Kafka cluster
input topics and changelog topics"] --- I1 K --- I2 K --- I3
Figure 2: a client may reach any instance. That instance answers from its own stores or forwards the request to the instance that owns the key.

Three instances share twelve partitions, four each. Customer 42’s key hashes to partition 5, which currently belongs to Instance B. A request for that customer can land on any instance, because a load balancer spreads requests. If it lands on Instance B, the answer comes straight from B’s local store. If it lands on Instance A, A must work out that partition 5 belongs to B and pass the request along.

1

Compute the partition for the key

Apply the same partitioning rule that was used when the events were written. Customer 42 maps to partition 5.

2

Find the instance that owns that partition

Ask the Streams library for the metadata of the key. It names the instance that currently holds the active copy.

3

Read locally or forward

If that instance is this one, read the local store. Otherwise send the request to the owner and relay its reply.

1 owner
for every key at any moment
Any
instance may receive the request
Yours
the forwarding layer is built by you

MMaking a Store Queryable: Names and Materialization

Not every store can be queried. Only stores with an explicit name are visible from outside the topology.

When a topology performs an aggregation, Kafka Streams creates a state store behind the scenes. By default that store has a generated internal name, and there is no reasonable way for outside code to refer to it. To make a store queryable, you give it a name when you define the aggregation, using a setting called Materialized. Naming it is the act of publishing it to the outside world.

Everyday analogy

Every shelf in the branch has a card index, but only those with a label on the front, such as “Customer totals”, appear in the city catalogue. An unlabelled index exists, but no visitor can ask for it by name.

What naming gives you

Lookup

A handle for queries

Outside code asks for the store by name and by type, and receives a read-only view.

Stability

A predictable changelog

The store’s backup topic also gets a stable name based on the application id and the store name, which helps operations.

Choice

Control over type and storage

Naming lets you choose the key and value formats, whether to keep the store on disk or in memory, and how long to retain it.

Discovery

Metadata per store

The library can tell you which instances host which partitions of each named store.

Which parts of a topology create queryable state

OperationResultStore type
Count or aggregate by keyA table of running resultsKey-value store
Aggregate within time windowsA value per key per windowWindow store
Aggregate within activity sessionsA value per key per sessionSession store
Read a topic as a tableThe latest value per keyKey-value store
Read a topic as a fully replicated tableA full copy on every instanceKey-value store, global
!
Decide on queryability at design time

Changing store names later changes the changelog topic names, which normally means the application starts from empty state. Choose clear, permanent names from the start.

YStore Types and What You Can Ask

Different stores answer different questions. The type you choose decides which questions are cheap.

Store typeShape of the dataQuestions it answers well
Key-value storeOne current value per keyGet one key, get many keys, scan a range or a prefix, scan everything
Timestamped key-value storeValue plus the time it was last updatedThe above, plus “how fresh is this value?”
Window storeOne value per key per time windowAll windows for a key in a time range, or all keys in a window
Session storeOne value per key per activity sessionAll sessions for a key
Versioned key-value storeSeveral values per key, each valid for a time span“What was the value of this key at time T?”

Point lookups versus scans

The cheapest question is a point lookup: give me the value of exactly this key. It touches one partition on one instance and returns quickly. A range scan asks for all keys between two values, and because stores keep keys in sorted order, this is efficient inside one store. But if the keys of interest are spread over several partitions, the scan must be sent to every instance that holds one of them and the results merged. A scan of everything touches every instance.

Cheap

Point lookup

One partition, one instance, one read. Suitable for user-facing request paths.

Moderate

Range or prefix scan in one partition

Efficient if the keys are designed so that related items sit together in the same partition.

Expensive

Scan across all partitions

Fans out to every instance. Fine for occasional tools, risky on a hot request path.

Special

Time-travel lookups

Versioned stores answer questions about past values without replaying the log.

i
Design keys for the questions you will ask

If you often need “all orders of customer 42”, make the customer id the key or its prefix, so those orders live together. Key design is the main tool for making queries cheap.

LQuerying the Local Store

Let us begin with the simplest case: the key is held by the instance you are talking to.

To read a store, your code asks the running Streams object for it, passing the store’s name and the kind of store it is. The result is a read-only view with methods such as get for one key, range for a span of keys, and all for everything. The view is live: it reflects the current state of the store, which keeps changing as events are processed.

1

Check that the application is ready

The Streams object must be running. During startup or restoration, a request for the store is refused with an error.

2

Obtain the store view

Ask by name and type. You receive a handle that covers all the partitions of this store hosted by this instance.

3

Look up the key

Call get. You receive the current value, or nothing if the key has never been seen or was deleted.

4

Return the result

Convert the value into whatever your callers expect, such as a JSON reply.

Rules for scans

Methods that return many entries, such as range and all, hand back an iterator. Behind that iterator sits a handle into RocksDB that holds memory and file resources. You must close the iterator when you finish. Forgetting to do so leaks resources, and in a busy service can slowly exhaust memory or make the store misbehave. Treat every iterator like an open file: use it, then close it, even when an error occurs.

Good habits

  • Close every iterator in a cleanup block
  • Limit the number of entries a scan may return
  • Use point lookups on request paths
  • Handle “key not found” as a normal answer

Risky habits

  • Leaving iterators open
  • Scanning everything on every request
  • Assuming a key you asked for must exist
  • Holding an iterator while doing slow work
!
A null answer is not always “no such key”

If the instance does not own the partition of the key, its local store will report nothing, even though the key exists elsewhere. Always check ownership before trusting an empty answer. The next chapters explain how.

DDiscovery: Finding the Instance That Owns a Key

The library knows where every partition lives. Your code only has to ask, and to tell the library how to be reached.

Every Kafka Streams instance is a member of a group, and the group knows which instance holds which tasks. To make that knowledge useful for queries, each instance can announce an address at which it accepts outside requests. This is done with a setting called application.server, whose value is a host and port such as “orders-2.internal:8080”. The setting is only a label: Kafka Streams does not open that port or listen on it. It simply shares the text with the other members, so they can read it from the group metadata.

Everyday analogy

Each branch writes its phone number on the city catalogue. The catalogue does not answer the phone; it only lists which branch holds which shelves and what number to dial. You still need to install the telephone.

Asking who owns a key

Given a store name, a key and a way to turn the key into bytes (a serializer), the library can compute the partition and answer with a KeyQueryMetadata object. It holds three useful things: the address of the instance with the active copy, the addresses of instances with standby copies, and the partition number. Your code compares the active address with its own. If they match, it reads locally. If not, it forwards the request.

Piece of informationWhat it tells youTypical use
Active hostThe instance that processes this partition and holds the freshest storeDefault target for a query
Standby hostsInstances that keep a warm copy of the same partitionFallback when the active host is unavailable
PartitionThe partition that the key maps toLogging, and per-partition lag checks
All-metadata listingEvery instance and the store partitions each one holdsFan-out for scans and status pages

Two ways to get it wrong

!
The serializer must match

If the key is turned into bytes differently from how it was when written, the library computes the wrong partition and points you at an instance that does not hold the key. The answer will be “not found” for data that exists. Use the same key format as the topology.

!
Custom partitioners need care

If the application writes to the input topic with a custom partitioning rule, the discovery call must be given the same rule. Otherwise it assumes the default hashing and computes the wrong partition.

Setting

application.server

A host and port each instance advertises. Must be reachable by other instances and by whoever forwards requests.

Lookup

Metadata for a key

Returns the active host, the standby hosts and the partition for one key.

Listing

Metadata for a store

Returns every instance that hosts any part of the store. Needed for scans.

RThe Remote Layer: The Part You Build

Kafka Streams stops at discovery. The wiring between instances is an ordinary web service that you design.

Each instance must run a small server, usually a REST or gRPC service, at the address it advertised. This service has two jobs. The first is to handle requests from clients and answer them, by reading locally or by calling another instance. The second is to handle requests from other instances that were forwarded to it and must not be forwarded again.

Job one

Public endpoint

Accepts requests such as “get total for customer 42”, finds the owner, and returns the answer.

Job two

Internal endpoint

Accepts forwarded requests and answers strictly from the local store, never forwarding again.

Why the two endpoints matter

Imagine ownership changes in the middle of a request. Instance A forwards to B, but B has just given the partition away to C. If B also forwarded, we could end up in a chain, or even a loop between two instances that each believe the other is the owner. The safe rule is that a forwarded request is answered locally or refused with an error, and the caller decides whether to retry. Keep the forwarding depth to exactly one hop.

Single hop, local only on the internal endpoint

  • No forwarding loops
  • Predictable latency
  • Easy to reason about failures
  • Clear place to count hops and errors

Unlimited forwarding

  • Loops during rebalances
  • Latency multiplies with each hop
  • Failures are hard to trace
  • One slow instance can stall many callers

Choosing the protocol

OptionStrengthsWatch out for
REST over HTTPSimple, easy to debug, works with every clientText encoding cost, less strict contracts
gRPCCompact, fast, strongly typed contractsMore tooling and learning curve
Client-side routingClient asks a metadata endpoint, then talks directly to the ownerClients must handle changes in ownership
Server-side forwardingClients are simple and see one addressExtra hop, and each instance needs client code for its peers
i
Start with forwarding

Most teams begin with server-side forwarding behind a load balancer, since clients stay simple. Client-side routing is a later optimisation for very hot paths.

JThe Journey of One Request

Let us follow a single question all the way through, including the case where it lands on the wrong instance.

sequenceDiagram
  participant C as Client
  participant A as Instance A
  participant B as Instance B
  C->>A: Get total for customer 42
  A->>A: Ask library for owner of key 42
  A-->>A: Owner is Instance B, partition 5
  A->>B: Internal request for key 42
  B->>B: Read local store
  B-->>A: Total is 1250
  A-->>C: Total is 1250
Figure 3: the request lands on A, which discovers that B owns the key and forwards a single internal request.
1

Client sends the request

The load balancer chooses any healthy instance. The client neither knows nor cares which.

2

Instance checks its own readiness

If its Streams state is not running, it should refuse quickly with a “try again” response.

3

Instance asks who owns the key

This is a local computation over cached group metadata, so it is very cheap.

4

Local or remote

If the owner is this instance, read locally. Otherwise call the owner’s internal endpoint.

5

Owner answers from its store

It confirms it still owns the partition, reads the value, and replies.

6

Result returned

The first instance relays the answer, or a clear error such as “not ready” or “moved, retry”.

Local
reads are microseconds to a millisecond
+1 hop
adds a network round trip when forwarding
≈1 in N
chance that a random request is already local with N instances

With three instances and a random balancer, about one request in three lands on the owner, and two in three need a hop. With ten instances, nine in ten need one. This is why forwarding must be efficient, and why some teams later switch to client-side routing or a partition-aware load balancer.

Everyday analogy

You walk into the nearest library. The desk checks the city catalogue, sees the book is at another branch, phones them, and reads you the answer. The more branches there are, the more often the book is elsewhere.

IInstance State and Readiness

A Streams instance moves through named states. Queries are only safe in some of them.

The KafkaStreams object has a state that you can read and observe. Understanding the states tells you when queries will succeed and when they will fail.

stateDiagram-v2
  [*] --> CREATED
  CREATED --> REBALANCING: start
  REBALANCING --> RUNNING: assignment done and stores restored
  RUNNING --> REBALANCING: membership change
  RUNNING --> PENDING_SHUTDOWN: close
  REBALANCING --> PENDING_SHUTDOWN: close
  RUNNING --> PENDING_ERROR: fatal error
  PENDING_ERROR --> ERROR
  PENDING_SHUTDOWN --> NOT_RUNNING
Figure 4: the main states of a Kafka Streams instance. Queries are normally safe only in RUNNING.
StateMeaningQueries
CREATEDObject built but not startedRefused
REBALANCINGJoining the group, receiving tasks, restoring storesUsually refused, or possibly stale if allowed
RUNNINGProcessing normallyAllowed
PENDING_SHUTDOWN, NOT_RUNNINGStopping or stoppedRefused
PENDING_ERROR, ERRORFatal failureRefused; the instance must be restarted

The errors you will meet

Not started

Streams not started

The store was requested before the instance reached a state where queries work.

Moved

Store migrated

The partition was reassigned to another instance since you obtained the store handle. Discover again and retry.

Not here

Store not available

The store exists but is not currently hosted or ready on this instance, for example while it restores.

Unknown

Unknown store

The name does not exist in the topology. This is a programming mistake, not a transient condition.

i
Wire state to your health checks

Make the readiness probe of the container report ready only when the Streams state is RUNNING. Then the load balancer sends no queries to an instance that cannot answer them. Keep the liveness probe separate, so a long restoration does not get the container killed.

!
Do not cache the store handle for long

A store view obtained before a rebalance may point to partitions that have since moved. Obtain a fresh view for each request, or handle the “migrated” error by getting a new one.

MMigration: When Rebalances Move Stores Around

Instances come and go. Each change shuffles who owns what, and queries must survive it.

A rebalance reassigns tasks among instances. It happens when an instance joins, leaves, crashes or is unresponsive for too long, or when the input topics gain partitions. When a task moves, its state store moves with it, in the sense that the new owner must have a copy. It either restores that copy from the changelog, or, if it kept a standby replica, catches up from a nearly-ready copy.

Everyday analogy

The city reorganises which branch looks after which shelves. During the move, the old branch stops answering questions about those shelves, and the new branch is not ready until the cards are re-filed from head office copies. Visitors are told “please ask again shortly”.

flowchart TD
  Q["Query arrives"] --> R{"Instance state is RUNNING?"}
  R -- "No" --> N["Return not ready, client retries later"]
  R -- "Yes" --> D["Discover owner for key"]
  D --> L{"Owner is this instance?"}
  L -- "Yes" --> S["Read local store"]
  L -- "No" --> F["Forward once to owner"]
  S --> E{"Store migrated or unavailable?"}
  F --> E
  E -- "Yes" --> W["Wait briefly, discover again, retry a few times"]
  W --> D
  E -- "No" --> A["Return answer"]
Figure 5: a resilient query loop that copes with instances being unready and partitions moving.

What a good retry policy looks like

1

Recognise transient errors

Not started, migrated and not available are all temporary. An unknown store is not.

2

Back off briefly

Wait a short time, growing on each attempt, with a little randomness so many callers do not retry at the same moment.

3

Discover again before retrying

The owner may have changed. Ask the library again rather than repeating the old target.

4

Give up with a clear message

After a small bounded number of tries, return a “temporarily unavailable” response rather than hanging.

Seconds
typical unavailability with standby replicas
Minutes+
possible when a large store restores from scratch
i
Static membership and cooperative rebalancing help

Giving instances stable identities and using the incremental rebalancing protocol shrink the number of tasks that move during a rolling restart, and so shrink the window in which queries fail.

SStale Reads, Standbys and Lag

Sometimes a slightly old answer now is better than no answer. Kafka Streams lets you choose, and measure how old.

By default, a query against a store that is being restored or has just moved fails. That protects you from reading incomplete data, but it makes the service unavailable during every rebalance. Since version 2.5, an option lets queries read from stores that are not yet fully up to date, which is called allowing stale stores. This can include standby copies that live on other instances.

Standby replicas

A standby replica is a hot spare of a task’s store. It does not process input or produce output, but it keeps reading the changelog and applying updates. You choose how many with the setting num.standby.replicas. If the active instance dies, a standby is promoted quickly, since most of its data is already in place.

With standby replicas

  • Much faster failover, since little needs restoring
  • A second place to read from during trouble
  • Smoother rolling restarts

What they cost

  • Extra disk for every copy
  • Extra network and CPU to keep copies updated
  • Reads from them may lag behind the active copy

Measuring how far behind

Lag here means the number of changelog records a local store copy has not yet applied. The library can report the lag of every local store partition, active or standby. A caller can then say: “I will accept an answer from a standby, provided it is within 1,000 records of the latest.” Without measuring lag, “stale” is a guess.

flowchart TD
  Q["Query for key"] --> A{"Active host reachable and ready?"}
  A -- "Yes" --> RA["Read from active: freshest answer"]
  A -- "No" --> ST{"Standby exists and lag within limit?"}
  ST -- "Yes" --> RS["Read from standby: possibly slightly stale"]
  ST -- "No" --> ER["Return temporarily unavailable"]
Figure 6: a decision flow that prefers freshness, tolerates a bounded amount of staleness, and otherwise fails clearly.
Setting or conceptWhat it does
num.standby.replicasHow many standby copies of each store to keep
Allow stale stores (query option)Lets queries read stores that are restoring or standby
Local lag informationReports how far each local store partition is behind its changelog
acceptable.recovery.lagHow close a copy must be before it counts as caught up for assignment purposes
max.warmup.replicasHow many extra copies may be built in the background while tasks move
probing.rebalance.interval.msHow often the group checks whether warm-up copies have caught up
Everyday analogy

The nearest branch is closed for renovation. Another branch keeps a photocopy of its index, updated every few minutes. You may prefer the photocopy over waiting, if you know it is only a few minutes old and your question can tolerate that.

i
Let the caller choose

A dashboard may happily accept slightly old data. A payment check should not. Expose a parameter or separate endpoint so each caller picks its own freshness rule.

CConsistency: How Fresh Is “Fresh”?

A query answers from the store as it is at that moment, which is not always the same as what has been safely committed.

Stream processing is continuous. A task reads a record, updates its store, and writes to the changelog and output topics, and commits its progress every so often. A query reads the store at some instant, and that instant may fall between updates and commits. So a query can see values that reflect processing that is not yet committed. If the task then fails and restarts from its last commit, those updates are reprocessed, and the store may briefly move backwards before moving forward again.

Reads

Read-your-latest is not promised

A value written to the input topic a moment ago may not yet be reflected. The delay is the time to arrive, be processed and be cached.

Caching

Record cache delays visibility

Updates may sit in an in-memory cache until it is flushed at commit time or when full. Queries see the store after flushing, not the cache in flight.

Failure

Rewinds after a crash

Under some processing settings, a restart may make a value go back to an older state before catching up.

Across keys

No snapshot across partitions

Two keys read from different partitions reflect different moments. Sums across keys are approximate.

!
Exactly-once processing does not make queries transactional

Exactly-once settings guarantee the effects of processing in topics, not that outside readers only see committed state. Isolation of queries from uncommitted changes has been an area of ongoing improvement in Kafka Streams, so check the documentation of your version before relying on any particular behaviour.

Use caseTolerates staleness?Advice
Live dashboard of countsYes, seconds are fineAllow stale reads, show a “last updated” time
Customer-facing order statusMostlyPrefer active hosts, retry briefly on migration
Fraud check before approvalRarelyUse the active host only, fail rather than guess
Financial balance shown as authoritativeNoQuery the system of record instead
Interactive Queries answer “what does the stream job believe right now?”, not “what is permanently true?”
Everyday analogy

The librarian keeps a working index that is updated as books arrive, and only initials the official ledger every few minutes. If you ask at the desk, you see the working index. It is very current, but a fire could send the librarian back to the last initialled ledger.

IIQv2: The Newer Query API

The original query interface is small and fixed. A newer generation adds richer requests and more information in each reply.

The first interface, often called IQv1, gives you read-only store views with a fixed set of methods. It works but has limits: you cannot ask for how fresh the answer is, you cannot express new kinds of queries without changing the library, and custom stores are hard to support. A newer interface, called IQv2, appeared in the 3.x releases as a redesign. The idea is that a query is now an object you build and send, and stores that understand that kind of query answer it.

Request

Query objects

You describe what you want, such as a key lookup or a range, as an object, rather than calling a fixed method.

Position

Position bounds

You can say “only answer if you have processed at least up to this point”, which gives you a form of read-your-writes.

Result

Per-partition results

Each reply says which partition it came from and how far that store had progressed.

Extensible

Custom query types

Store authors can support their own kinds of queries without changing the library.

Everyday analogy

The old interface is like a fixed set of buttons on a vending machine. The new one is a counter where you hand over a written order, and the reply comes with a receipt saying how up to date the shelf was.

Why position matters

Suppose a user submits an order and immediately views their totals. With plain queries, the total might not yet include the order. If the write operation gives back the position of the event in the topic, the read can carry that position and ask the store to answer only once it has caught up. That is far better than guessing with delays.

!
Check your version

IQv2 has been evolving over several releases. Some query types and features may be marked as evolving or limited to certain store types. Read the release notes of the version you use before designing around it, and keep your query logic behind a thin internal interface so you can adapt.

Benefits

  • Freshness information in each result
  • Read-your-writes through position bounds
  • Room for new query types

Costs

  • Newer, and still evolving
  • Migration effort from the older interface
  • Fewer community examples

GGlobalKTable: A Full Copy Everywhere

Sometimes it is easier to give every instance all the data than to work out who owns which part.

A GlobalKTable is a table built from a topic in which every instance holds every partition, not just its own share. Because each instance keeps a complete copy, a lookup of any key can always be answered locally. No discovery and no forwarding are needed.

AspectRegular table (KTable)Global table (GlobalKTable)
Data on each instanceOnly its assigned partitionsAll partitions
Lookup of any key locallyNoYes
Storage costTotal size, divided among instancesTotal size, on every instance
Fits huge data setsYesOnly if the data fits on one machine
Best forLarge, partitioned stateSmall, slow-changing reference data
Everyday analogy

Instead of splitting the phone directory between branches, head office sends every branch a full copy. Any desk can answer any question at once, at the price of a bookcase per branch.

When it is a good fit

Reference data lookups

Country codes, product catalogues of modest size, feature flags, tariff tables. Every instance can enrich or answer from the table with no network hop.

!
Not for big or fast-changing state

Every instance must consume every update and store every key. If the data set is large or changes rapidly, the cost grows with the number of instances rather than being shared. Also, global tables are loaded and updated separately from normal task processing, so their timing relative to the main stream is looser.

SScans: Range, Prefix and Full

Reading many keys at once is where local queries turn into cluster-wide work.

A scan reads a run of entries in key order. Inside one store partition this is fast. But your question may cover keys living on many partitions, and therefore many instances. The way to handle this is called fan-out and merge: ask each instance that hosts part of the store for its share, then combine the answers.

1

List the hosts of the store

Ask the library for all instances that hold any partitions of this store.

2

Send each a local-only scan

Each instance scans just its own partitions and replies with a bounded result.

3

Merge the partial results

Combine, sort if needed, and apply the overall limit.

4

Tolerate missing shares

Decide up front whether one unreachable instance fails the whole request or returns a partial answer marked as such.

Scan typeReachGuidance
Prefix scan with a key that includes the partition keyOne partitionGood on request paths
Range scan on partition keyPossibly many partitionsFan out, limit the size
Full scanEvery partitionAdmin tools and reports only
!
Scans compete with processing

A scan uses disk reads and memory on the same instance that is processing events. A heavy scan can slow the stream job itself and raise its lag. Always cap the number of entries, apply timeouts, and consider rate limits on scan endpoints.

i
Pre-compute instead of scanning

If dashboards need “top ten customers”, make the stream job maintain that ranking as its own small store, and read one key. Scanning a million entries per page view is the wrong tool.

WWindowed and Session Queries

When results are grouped by time, a key has many values, one per window. Queries must say which times they want.

A window is a slice of time, such as “10:00 to 10:05”. A windowed aggregation keeps one result per key per window, for example the number of page views per user per five minutes. A session is different: it is a burst of activity for a key that ends after a gap of silence, so sessions vary in length. Both store several values for one key, so a lookup needs a key and a time range.

Window

Fixed or sliding time slices

Ask for one key across a time range and you get one value per window that overlaps it.

Session

Activity bursts

Ask for one key and receive each session with its start, end and value.

Retention

Old windows disappear

Windows are kept for a configured retention period, then removed. Old data may no longer be queryable.

Grace

Late events still update

A window can still change while late events are accepted, so a recent window’s value may grow.

Everyday analogy

A shop keeps a tally sheet for each hour of the day. To learn how busy Tuesday was, you read several sheets. Old sheets are shredded after a month, and a late receipt can still be added to a recent sheet.

Practical rules

1

Always bound the time range

Asking for a key’s entire history from the beginning of time is a scan in disguise.

2

Make retention match your questions

If users may ask for the last 30 days, retention must be at least that long, and disk must cover it.

3

Show window boundaries to users

Return the start and end times with each value so nobody mistakes a partial, still-open window for a final one.

i
The open window is special

The most recent window is still collecting events. Label it “in progress” in user interfaces, or exclude it from reports that must be final.

FFour Failure Stories

Real problems teach best. Each story below is typical of what teams report, told simply.

Story 1Wrong answer: “not found”
What happened

A service returned “no such customer” for customers who clearly existed. It happened only for some requests, at random.

Why

The service read its own local store without checking ownership. When the request landed on an instance that did not own the customer’s partition, the local store was empty for that key.

Fix

Always ask the library who owns the key and forward when it is not this instance. Treat “not found” as valid only from the owner.

Story 2Errors during every deployment
What happened

Each rolling deployment produced a burst of errors that lasted several minutes.

Why

Instances were replaced with no standby replicas, and restarted with empty disks. Each new instance had to rebuild its stores from the changelog while the load balancer already sent it traffic.

Fix

Tie readiness to the RUNNING state, keep local disks across restarts where possible, add standby replicas, and replace one instance at a time.

Story 3Slow processing after a report was added
What happened

After an “export all customers” endpoint was released, stream processing lag rose sharply each time someone used it.

Why

The export scanned every store on every instance, competing for disk and memory with processing. Iterators were also left open on error paths.

Fix

Cap and paginate scans, rate limit the endpoint, close iterators in cleanup blocks, and move exports to a separate read-only copy of the data.

Story 4A loop of forwarded requests
What happened

During a rebalance, latency spiked and threads piled up across several instances.

Why

The internal endpoint also used the discovery call and forwarded when it thought it was not the owner. Two instances each believed the other was the owner and passed the request back and forth.

Fix

The internal endpoint must answer only from local state, or return “moved”. Never forward twice.

i
The common thread

Every story comes from treating a distributed system as if it were a single database. The instance in front of you is only one shelf of many, and it may not even be stable.

PPerformance: What Makes Queries Fast or Slow

Local reads are very fast. Almost every slowdown comes from something around the read.

FactorEffectWhat to do
Local point lookupVery fast, often well under a millisecond when data is cachedKeep hot keys in the block cache with enough memory
Extra network hopAdds a round trip per forwarded requestUse keep-alive connections and connection pools
SerializationCPU cost of turning bytes into objects and backUse compact formats, avoid converting large values needlessly
Disk reads in RocksDBSlower when data is not in memorySize memory for the working set, use fast disks
Competing with processingQueries and stream work share threads, disk and memoryLimit query concurrency, use separate thread pools
Restoration in progressStore is not usable, disks are busyStandby replicas, faster disks, readiness gating
Tiny
local RocksDB point read from cache
Small
extra cost of one internal hop in the same network zone
Large
cost of an unbounded scan

Ways to add capacity

More instances

Spread the partitions

Up to the number of tasks, more instances means each holds less data and serves fewer keys.

More partitions

Plan ahead

Adding partitions later changes which partition each key maps to, and requires rebuilding state. Choose a generous number early.

Caching

Cache popular answers

A short-lived cache in front of the endpoint absorbs repeated requests for the same key.

Standbys

Read replicas

Where staleness is acceptable, allow standby copies to serve reads and spread load.

!
Scaling reads scales the stream job too

Query load lands on the same instances that process events. There is no separate read tier, so a surge of queries can slow processing. Watch both together.

TTrade-offs: Interactive Queries or an External Database?

Neither is always right. The choice depends on what you ask, how fresh it must be, and what you are willing to operate.

QuestionInteractive QueriesExternal database fed from Kafka
Extra systems to runNone beyond the Streams appA database, and a connector or writer
FreshnessVery high, straight from processingDelayed by the extra hop
Query flexibilityLookups and ranges over keys you designedRich queries, indexes and joins
Independent read scalingNo, shares instances with processingYes, scale the database separately
Availability during deploymentsDepends on your readiness and standbysDatabase stays up while the job restarts
Client accessYou build and secure the serviceStandard drivers and tools
Consistency storyReads may see uncommitted or moving stateReads see what was written

Choose Interactive Queries when

  • Questions are key lookups on results you already compute
  • Freshness matters more than rich querying
  • You want fewer moving parts
  • Your team is comfortable operating stateful services

Choose an external store when

  • You need ad hoc or analytical queries
  • Reads must stay up while the job redeploys
  • Many different teams and tools need access
  • Read volume is far larger than processing volume
Everyday analogy

Asking the librarian who is filing the books gives you the freshest answer, but you must wait when they are busy or move. Asking a separate reference desk that receives copies of every change gives you a slower, steadier answer and many more ways to search.

A middle path

Many organisations do both. The stream job powers a fast, narrow lookup endpoint for a user-facing feature, and also writes its results to a database for reporting. The lookup service is the sharp tool for one job, and the database is the wide tool for everything else. Related tools, such as ksqlDB’s pull queries, are built on the same underlying idea, hiding the discovery and forwarding work behind a query language.

i
Ask three questions

What exactly will be asked? How stale may the answer be? Who must be able to reach it? If the answers are “one key at a time”, “seconds”, and “our own service”, Interactive Queries fit well.

SSecurity: Guarding the Query Door

The moment you add an endpoint, you own a new attack surface. Kafka’s own protections do not cover it.

Kafka can require clients to prove who they are, encrypt traffic, and limit which topics each user may read or write. None of that applies to the web endpoint you built for queries. Anyone who can reach that endpoint may read whatever it exposes, regardless of who is allowed to read the underlying topics.

Identity

Authenticate callers

Require a token or certificate on the public endpoint, and check who is asking on every request.

Permission

Authorize each lookup

A user may see their own record, not another’s. Enforce that in the service, not in the client.

Privacy

Avoid exposing sensitive fields

Return only what is needed. A store may hold personal or confidential values that the API should mask or omit.

Transport

Encrypt everything

Use TLS on both the public endpoint and the instance-to-instance internal one.

Internal

Protect forwarded calls

The internal endpoint must accept only trusted peers, for example through mutual TLS or network rules, and must not be reachable from the internet.

Abuse

Limit and monitor use

Apply rate limits and size caps, and log access, so one caller cannot scrape or overload the store.

!
Scan endpoints are data-exposure risks

An endpoint that lists all keys, or a prefix, can leak the entire data set to an authenticated but unauthorised user. Restrict such operations to administrators, and prefer exact-key lookups for ordinary callers.

!
Injection and enumeration

Treat keys received from clients as untrusted input. Validate their format and length. Sequential ids let attackers walk through all records, so pair them with authorization checks or use unguessable identifiers.

Everyday analogy

The city library requires a card to borrow books. But if a branch opens a side window that answers any question shouted at it, the card rule no longer protects anything. The window needs its own guard.

MMonitoring, Logging and Metrics

You cannot fix what you cannot see. Watch the stream job and the query service together.

AreaWhat to watchWhy it matters
Instance stateCurrent state and number of rebalancesQueries fail outside RUNNING; frequent rebalances mean unstable service
Store metricsGet and range request rates and latencies per storeShows load and slow reads
RestorationRestore progress, records restored, time spentExplains long unavailability
LagConsumer lag and local store lagTells how stale answers may be
End-to-end latencyTime from event creation to being reflectedMeasures freshness for users
RocksDB internalsCache hit ratio, pending compactions, memory (more detailed metrics are usually off by default)Predicts disk pressure and slow reads
Your serviceRequests, errors, retries, forwarded hops, timeoutsKafka Streams cannot see these for you

Application-level signals to add

Counter

Local versus forwarded

The ratio shows how much load goes over the network and whether routing improvements would help.

Counter

Retries and give-ups

Rising numbers reveal rebalances or unstable instances.

Histogram

Latency by path

Track local reads and forwarded reads separately to see where time goes.

Gauge

Open iterators

A number that only grows means iterators are not being closed.

i
Log the owner and the outcome

For each request, log the key’s partition, the owner instance, whether it was local or forwarded, and the result. This makes ownership-related bugs easy to trace. Do not log sensitive values themselves.

!
Metric names differ between versions

Kafka Streams has reorganised its metrics over releases, and some are only available at detailed recording levels. Check the documentation for your version, and confirm each metric exists before building alerts on it.

RReal-World Uses

Interactive Queries are used where a fast answer about “the current state of X” matters.

Order and delivery status

A stream job combines events from ordering, payment and shipping into one status per order. A lookup service lets the app show “out for delivery” without a separate status database.

Live counters and leaderboards

Counts per product, per user or per region, updated continuously, and read by dashboards that refresh every few seconds.

Fraud and risk features

Recent activity counts per card or account, held in windowed stores, and consulted by decision services as one input among several.

Inventory and availability

Current stock per item location, computed from movement events, and queried by storefronts for a display value. Final reservation still goes through a system of record.

Session and user activity

Session stores hold each visitor’s current session summary, used to personalise pages while they browse.

Database-like query layers over streams

Products such as ksqlDB expose pull queries over materialized tables, using the same discovery-and-routing ideas so that users can write simple queries instead of building a service.

i
About company examples

Many companies have described using queryable state in talks and blog posts, but architectures change and details vary. Treat these patterns as typical, not as a record of any one organisation’s current setup.

DDesign Patterns and Anti-Patterns

A few shapes recur in good designs, and a few mistakes recur in bad ones.

Patterns that work

Pattern

Materialized view

The stream job maintains a ready-made answer per key. Queries only read it.

Pattern

Single-hop forwarding

Public endpoint discovers and forwards once. Internal endpoint answers locally only.

Pattern

CQRS style split

Writes go through Kafka as events. Reads come from views built by the stream job.

Pattern

Readiness gating

Traffic is sent only to instances in the RUNNING state.

Pattern

Precomputed aggregates

Top-N lists and totals are stored as their own keys, so no scan is needed.

Pattern

Read-through fallback

On failure, fall back to a database copy or a clear “try later”, never to a guess.

flowchart LR
  W["Write side: services publish events"] --> T["Kafka topics"]
  T --> S["Kafka Streams job builds views"]
  S --> V["Named state stores"]
  V --> Q["Query service: discover, read, forward"]
  Q --> U["Users and other services"]
  S --> DB["Optional database for reports"]
Figure 7: a common architecture. Events flow in, the job builds views, and a small query service exposes them.

Anti-patterns to avoid

AP-1Anti-pattern
Trusting the local store as the whole truth

Reading only the instance you reached and reporting “not found” for keys held elsewhere.

Better

Always discover the owner first.

AP-2Anti-pattern
The general-purpose database in disguise

Adding ad hoc query endpoints, filters and joins to a stream job until it becomes a poor database.

Better

Send analytical needs to a real analytical store.

AP-3Anti-pattern
Writing through the query path

Trying to change state via the store instead of by publishing an event. Stores are read-only for outsiders, and state must change only by processing events.

Better

Publish an event and let the job update the store.

AP-4Anti-pattern
Unbounded fan-out on a hot path

Every user request triggers a scan on every instance.

Better

Precompute, cap and rate limit.

KKeys and Partitioning in Depth

Almost every query problem traces back to how keys map to partitions. It is worth understanding the mapping precisely.

When a producer writes a message with a key, a partitioner turns the key into a number and picks a partition from it. By default, the key is converted to bytes, run through a hash function, and reduced to a partition number by taking the remainder after dividing by the partition count. The same key always gives the same partition, as long as the partition count and the rule stay the same. Kafka Streams uses the same rule when it re-keys data before an aggregation, which is why discovery can compute the owner of a key without asking anyone.

1

Key becomes bytes

A serializer converts the customer id to bytes. A different serializer, for example text versus number, gives different bytes.

2

Bytes become a hash

A fixed hash function turns the bytes into a large number.

3

Hash becomes a partition

The number is reduced to a value from zero up to the partition count minus one.

4

Partition becomes a task and an owner

Each partition belongs to one task, and each task is assigned to one instance at a time.

Re-keying and why it changes the answer

Sometimes the topology changes the key before aggregating, for example grouping orders by product instead of by customer. Kafka Streams then writes the re-keyed events to an internal repartition topic. The store you query is keyed by the new key, so discovery must be given the new key, not the original one. Asking for the total of customer 42 in a store that is keyed by product will find nothing.

ChangeEffect on queries
Add partitions to the input topicKeys map to different partitions, so existing state no longer lines up
Change the key serializerDifferent bytes, different partition, wrong owner computed
Change the grouping key in the topologyThe store is keyed differently, so queries must use the new key
Change the partitioner on the producerDiscovery must use the same custom rule
Everyday analogy

Shelves are chosen by the first letter of the surname. If the library suddenly starts filing by the first letter of the first name, every old lookup goes to the wrong shelf, even though no book was lost.

!
Hot keys

If one key receives far more events than others, its partition and its owner become a hotspot for both processing and queries. Watch for skew, and consider splitting the key, for example by adding a small suffix, when a single key dominates.

RRestoration Up Close

Restoration is the quiet period when a store rebuilds itself. Knowing what happens explains most availability gaps.

When an instance receives a task whose store is not present, or only partly present, it must catch up before processing. It first checks the local disk for a checkpoint file that records how far the local copy had reached in the changelog. If one exists, it reads the changelog from that position onward. If none exists, it reads from the very start. While this happens, the task does not process input records and the store cannot be queried normally.

flowchart TD
  T["Task assigned to instance"] --> C{"Local store and checkpoint exist?"}
  C -- "Yes" --> P["Read changelog from checkpoint position"]
  C -- "No" --> Z["Read changelog from the beginning"]
  P --> U["Apply records to the store"]
  Z --> U
  U --> D{"Caught up to the end of the changelog?"}
  D -- "No" --> U
  D -- "Yes" --> R["Task starts processing and store becomes queryable"]
Figure 8: a store either resumes from a saved position or rebuilds from the start, then the task begins normal work.
Fast
resume from a nearby checkpoint
Slow
rebuild from zero for a huge store

What makes it faster

Keep disks

Reuse local state

A restarted instance that finds its old files needs only a short catch-up.

Standbys

Stay nearly ready

A standby copy is constantly catching up, so promotion finishes almost at once.

Compaction

Smaller changelogs

Changelog topics are compacted, keeping the latest value per key, so a full replay is shorter than the total history.

Hardware

Fast disks and network

Restoration is bulk reading and writing, so throughput of storage and network sets the speed.

Everyday analogy

A new librarian receives a shelf. If the old card index is still in the drawer, they only add the last few weeks of changes. If the drawer is empty, they must copy every card from head office, one by one, before opening the desk.

i
Measure it before you need it

Time a full restoration in a test environment with production-sized data. That number decides how many standbys you need and how long deployments really take.

SSizing Memory, Disk and Threads

A queryable store is a database that shares a machine with a stream processor. Give it room to breathe.

RocksDB keeps recent writes in memory tables and organises older data in sorted files on disk. It uses a block cache, an in-memory area holding recently read pieces of files, so repeated reads avoid the disk. It also runs background compaction, tidying files to save space and keep reads quick. All of this uses memory, disk and CPU alongside your processing threads and query threads.

ResourceWho uses itSizing advice
Heap memoryYour service, Streams caches, serializationLeave room for bursts of concurrent queries
Off-heap memoryRocksDB tables and block cacheBound it explicitly, since each store can otherwise grow its own share
Disk spaceActive stores, standby stores, changelog restore dataPlan for active plus standby, plus compaction headroom
Disk speedReads on cache misses, compaction, restorationPrefer fast local storage
Processing threadsStream tasksMatch threads to cores and tasks
Query threadsWeb server workers and forwarding clientsUse a separate pool so queries cannot starve processing

A simple sizing walk

1

Estimate the data

Number of keys multiplied by the average size of key plus value gives the raw store size.

2

Add overhead

Stores use more space than raw data because of indexes and compaction. Allow a healthy margin.

3

Multiply by copies

Count the active copy plus each standby, and spread across instances.

4

Estimate the hot set

The portion of data read often should fit in the block cache, so most reads stay in memory.

Everyday analogy

A branch needs shelf space for its books, a second set of shelves for the sister branch’s copies, and a desk near the entrance for the most requested titles so the librarian does not run to the back every time.

!
Memory limits in containers

Off-heap memory counts against the container limit. If it is not bounded, the process can be killed for exceeding its limit even though the heap looks fine. Configure store memory deliberately.

EEvolving Schemas and Upgrading Safely

Stores live for a long time. Values written today must still be readable next year.

The values in a store are stored as bytes, produced by a serializer. If you change the shape of a value, for example by adding a field, the old bytes still sit in the store and the changelog. New code must be able to read them. Query responses also leak the shape of the value to clients, so a change to the value affects your API as well.

Compatible

Add optional fields

Old data lacks the field, and new code treats it as missing. This is generally safe with schema-aware formats.

Risky

Rename or retype fields

Old bytes no longer match new code. Plan a migration rather than a quiet change.

Boundary

Separate store type from API type

Map store values to a stable response format, so internal changes do not break clients.

Tooling

Use a schema registry

A registry with compatibility rules blocks changes that would break readers.

Upgrading the application

1

Read the upgrade notes

Kafka Streams versions sometimes change internal formats or metrics. Follow the documented upgrade path for your versions.

2

Upgrade one instance at a time

Watch the group settle and queries succeed before the next one.

3

Keep the query API stable

Clients and internal peers may run different versions during a rolling upgrade, so the internal endpoint must accept both old and new requests for a while.

!
Mixed versions during a rollout

While instances run different code, a forwarded request may hit a peer that does not understand a new field. Make internal messages backward compatible, and remove old behaviour only after every instance has been upgraded.

Everyday analogy

A library changes its card format. Old cards stay in the drawers for years, so the new cards must remain readable by staff trained on the old ones, and the change should be introduced gradually across branches.

CCaching and Load Shaping in Front of the Store

Because queries share resources with stream processing, protecting the store from unnecessary reads pays off twice.

Most real traffic is uneven: a small set of keys gets most of the requests. A short-lived cache in the query service, holding recent answers for a few seconds, absorbs repeated requests without touching the store or the network. The price is a small extra delay in freshness, so choose the lifetime to match what users can accept.

TechniqueBenefitCost
Short-lived answer cacheFewer store reads and hops for popular keysAnswers may be a few seconds older
Request coalescingMany identical concurrent requests share one readSlightly more code and care with errors
Rate limitingProtects processing from floodsSome callers see refusals
Timeouts on forwarded callsOne slow instance cannot stall all callersOccasional retries
Circuit breaker on a peerStops hammering an unhealthy instanceNeeds tuning to avoid false trips
Bulkheads (separate pools)Queries and internal calls cannot starve each otherMore settings to manage
Everyday analogy

The front desk keeps a notepad of the ten questions asked in the last minute. When the same question comes again, the answer is read from the notepad, and the librarian on the shelves is not disturbed.

Good load-shaping habits

  • Set short timeouts and bounded retries
  • Cache only what can tolerate delay
  • Separate thread pools for queries and forwards
  • Reject early when the service is overloaded

Habits that backfire

  • Unlimited retries that multiply load during trouble
  • Caching sensitive data without access checks
  • Long cache lifetimes on fast-changing values
  • Sharing one pool for every kind of call
i
Cache keys must include the caller when answers differ

If two users may see different views of the same key, the cache key must include the identity or permission level. Otherwise one user could receive another’s cached answer.

GGlossary of Terms in Plain Words

A quick reference for every important term used in this guide.

TermPlain meaning
TopicA named stream of events in Kafka
PartitionOne ordered slice of a topic, holding all events for certain keys
InstanceOne running copy of a Streams application
TaskThe unit of work that handles one partition of input
TopologyThe plan of processing steps in a Streams application
State storeA small local database that holds a task’s running results
Changelog topicA Kafka topic that backs up every change to a store
RestorationRebuilding a store by replaying its changelog
MaterializedA setting that names a store so that it can be queried
Interactive QueriesRead-only access to state stores from your own code
Active copyThe store copy held by the instance currently processing the task
Standby replicaA warm spare copy kept on another instance
LagHow far a copy is behind the latest changelog record
RebalanceReassigning tasks among instances
DiscoveryFinding which instance owns a key
ForwardingPassing a request to the instance that owns the key
Fan-outSending one request to many instances and merging the answers
GlobalKTableA table fully copied to every instance
WindowA slice of time used for grouping results
SessionA burst of activity ended by a gap of silence
ReadinessWhether an instance is currently able to answer requests
Stale readAn answer that may be slightly behind the latest state
i
If you remember only one picture

Many branches, each with a few shelves. A key belongs to exactly one shelf. Ask the catalogue where it is, phone that branch once, and be patient if the branch is being moved.

AAlternatives Worth Knowing: ksqlDB, Connect Sinks and Caches

Interactive Queries are one way to serve stream results. Here is how the neighbouring options differ, so you can choose calmly.

Three other approaches often appear in the same conversation. Each solves the “how do I read the answer” problem in a different place, and each moves the cost somewhere else.

Query language

ksqlDB pull queries

You describe a materialized table in SQL-like statements, and the server answers simple lookups on it. Discovery and forwarding are handled for you, but you accept the server’s limits on query shapes and its operating model.

Copy out

Connect sink to a database

A connector copies results into a database of your choice. You gain full query power and independent scaling, and you take on another system and a small delay.

Fast layer

Write results to a cache

The stream job writes computed values into an in-memory store such as a key-value cache. Reads are very fast and scale separately, but the cache is another copy to keep correct.

Direct

Interactive Queries

Reads come straight from the job’s stores. Fewest moving parts and freshest data, but you build the service and share resources with processing.

OptionYou buildYou operateQuery power
Interactive QueriesQuery service, routing, securityOnly the Streams applicationKey lookups and simple ranges
ksqlDB pull queriesStatements and a clientA ksqlDB clusterLookups on tables, with limits
Connect sink to databaseConnector configurationConnect and the databaseWhatever the database offers
Cache written by the jobWriter codeThe cache systemKey lookups
Everyday analogy

You can ask the librarian who files the books, hire a receptionist who knows the catalogue and speaks a simple query language, send a daily copy of the index to a separate reference desk, or pin the most popular answers on a noticeboard. Each is faster or slower, cheaper or dearer, and offers a different range of questions.

A short way to choose

1

List the questions

If they are all “give me the value for this key”, any option works and the simplest wins.

2

Decide who runs the reads

If the stream team will own the read path, Interactive Queries fit. If a platform or data team wants standard tools, a database or query server fits better.

3

Check the availability need

If reads must survive redeploys of the job, prefer a separate store that stays up on its own.

4

Start small and keep the door open

Because the job builds the view either way, you can begin with direct queries and add a sink later without rewriting the processing.

i
The view is the valuable part

The hard work is defining the right materialized view: correct keys, correct aggregation, correct time handling. Where you serve it from is a smaller and more reversible decision.

BBest Practices and Common Mistakes

A checklist of habits that keep a query layer healthy, and the slips that most often hurt.

Best practices

  • Name every store you plan to query, with a permanent name
  • Design keys so common questions touch one partition
  • Advertise a reachable address for every instance
  • Gate traffic on the RUNNING state
  • Forward at most once, and answer locally on the internal endpoint
  • Retry transient errors with backoff and fresh discovery
  • Use standby replicas for faster recovery
  • Close every iterator, cap every scan
  • Secure both the public and the internal endpoints
  • Track metrics for both the stream job and the service

Common mistakes

  • Reading the local store without checking ownership
  • Forgetting that a key’s serializer must match the topology
  • Caching a store handle across rebalances
  • Leaving iterators open
  • Allowing full scans on public endpoints
  • Treating query results as fully consistent snapshots
  • Not planning for partition count growth
  • Exposing the internal endpoint to the internet
  • Ignoring restoration time in capacity planning
  • Assuming the container’s liveness probe equals readiness

A short pre-launch checklist

1

Kill an instance under load

Watch how long queries fail and whether retries hide it from users.

2

Do a rolling restart

Confirm readiness gating and check the length of any error window.

3

Add and remove an instance

Confirm ownership changes are noticed and forwarded requests still succeed.

4

Test with realistic store size

Restoration time on a full-size store is the number that surprises teams most.

5

Try hostile requests

Bad keys, huge ranges, missing credentials, and floods of requests.

MMyths and Misunderstandings

Some ideas sound reasonable but are wrong. Clearing them up prevents expensive surprises.

MythReality
“I can query any Kafka topic with Interactive Queries.”Only state stores built by a Kafka Streams application are queryable. Plain topics are not.
“Kafka Streams will route my request for me.”It tells you who owns a key. Building the network layer is your job.
“An empty answer means the key does not exist.”It may mean the local instance does not own the key, or the store is not ready.
“Standby replicas make reads instant and always fresh.”They speed up failover. Reads from them may be behind the active copy.
“Exactly-once means my queries are transactional.”It concerns the effects of processing in topics, not read isolation for outside queries.
“I can write to the store through the query API.”The view is read-only. Change state by publishing events.
“Adding partitions later is harmless.”It changes the key-to-partition mapping and typically requires rebuilding state.
“Kafka ACLs secure my query endpoint.”They do not. The endpoint is your own service and needs its own security.
“Interactive Queries replace databases.”They suit key-based lookups on stream results. Many needs still call for a database.
The store is a reader’s window into the job’s memory, not a database that happens to live inside it.

CContainer Deployment and Rolling Restarts

Stateful services are harder to run than stateless ones. A few platform choices make a big difference.

Storage

Keep the state directory on durable disk

If a restarted instance finds its old store files, it only needs to replay the recent part of the changelog, not everything.

Identity

Stable names per instance

Workload types that keep the same name and disk for each replica work well with static membership and local state reuse.

Probes

Separate readiness and liveness

Ready only when RUNNING. Alive as long as the process is healthy, so a long restoration is not mistaken for a hang.

Shutdown

Close Streams cleanly

Handle the stop signal, close the Streams object, and allow enough grace time for it to finish.

Pace

One at a time

Replace one instance, wait until it reports ready, then move to the next.

Network

Advertise a reachable address

The advertised host and port must resolve from other instances. In containers, this is the pod or service address, not the loopback address.

SetupBehaviour on restart
Empty disk, no standbysFull restoration, long unavailability for the tasks it owns
Persistent disk, no standbysShort catch-up from the last position, but tasks are still offline meanwhile
Standby replicas, any diskStandbys are promoted quickly, a short pause only
Persistent disk, standbys and static membershipSmoothest: few tasks move, quick recovery
!
Wrong advertised address

A frequent deployment error is advertising “localhost”. Discovery then tells other instances to call themselves, and every forwarded request fails or loops. Test forwarding across real, separate instances before release.

Everyday analogy

When a branch is closed for a week, its shelves can stay put if the building remains and only the staff change. If the building is demolished, everything must be rebuilt from head office. Persistent disks are the building, standbys are a sister branch with copies.

TTesting Your Query Layer

Tests should cover the topology, the store reads, and the ugly moments when ownership changes.

1

Unit test the topology

Kafka Streams provides a test driver that runs a topology without brokers and gives access to its stores, so you can feed events and check values.

2

Test the query service logic

Use fake discovery results to check local reads, forwarding, retry and error mapping without a cluster.

3

Integration test with several instances

Run at least two instances against a test cluster. Check that a key is answered correctly whichever instance is asked.

4

Chaos test

Stop and start instances during queries. Confirm bounded errors, recovery and no forwarding loops.

5

Load test

Measure latency and stream lag together under realistic query rates.

Test levelCatchesDoes not catch
Topology unit testWrong aggregation, wrong keys, wrong store namesOwnership, network, rebalances
Service logic testRetry mistakes, error mapping, loopsReal timing and load
Multi-instance integrationDiscovery and forwarding errors, wrong serializersProduction-scale restoration
Chaos and loadRecovery windows, resource contentionRare data-dependent bugs
i
Test with a small partition count deliberately

With only one partition, everything is always local and forwarding bugs stay hidden. Test with several partitions and at least two instances so ownership actually varies.

PPutting It All Together: A Worked Example

Let us design the customer order-total feature from the start of this guide, end to end.

1

Choose the key

Orders are keyed by customer id, so all of one customer’s orders share a partition.

2

Build and name the aggregate

Group orders by customer and sum the amounts into a named store, “customer-totals”.

3

Configure the instances

Twelve partitions, three instances, one standby replica each, a reachable advertised address, and persistent disks.

4

Build the service

A public endpoint that discovers and forwards once, and an internal endpoint that answers locally only.

5

Secure it

Require login, allow customers to read only their own total, use TLS, and keep the internal endpoint private.

6

Make it resilient

Readiness follows the RUNNING state. Transient errors are retried with backoff. Slightly stale reads from standbys are allowed within a small lag limit for display purposes.

7

Watch it

Track state, restoration, lag, request latency, forward ratio and retries, with alerts on sustained error rates.

12
partitions and tasks
3
instances, four active tasks each
1
standby copy of every task
≈2/3
of requests need one forwarded hop

What we sized

Suppose each of the 12 tasks holds about one million customers, with a few hundred bytes per entry. The active data is roughly a few gigabytes per task in total across the cluster, and the standby adds a second copy. Each instance holds four active and four standby stores, so plan disk for eight stores’ worth, plus room for compaction. A restart with persistent disks replays only the recent changes, while a lost disk replays the entire changelog for that task, which is why standbys are worth their extra space.

i
When it grows

If query volume grows beyond what the instances can serve while processing, consider allowing standby reads, adding a short-lived cache, or copying results to a database for the heaviest readers. The design is easy to evolve because the stream job stays the single source of the view.

FFrequently Asked Questions

Q1

Do Interactive Queries need extra infrastructure?

No new cluster is needed. They are part of the Streams library. You do need to build a small service around them to accept requests and route them between instances.

Q2

Can I query a KStream?

A stream itself is not stored, so it cannot be queried. You query the tables and stores created by aggregations, table reads or materialized results.

Q3

What happens to queries during a rebalance?

By default, stores that are moving or restoring reject queries with a temporary error. Your service should retry after a short delay, or, if you allow stale reads, use a standby within an acceptable lag.

Q4

How do I know which instance owns a key?

Ask the library for the metadata of the key, giving the store name, the key and its serializer. It returns the active host, standby hosts and the partition.

Q5

Is the data in the store the same as what has been committed?

Not necessarily. Queries may see updates that are processed but not yet committed. If the task fails, it restarts from its last commit, and the value may briefly step back. Check the documentation of your version for the current guarantees.

Q6

Can I use a GlobalKTable to avoid forwarding?

Yes, for small, slowly changing data that fits on every instance. It is not suitable for large or fast-changing data sets.

Q7

Should the public endpoint be exposed directly to browsers?

Usually it should sit behind a gateway that handles authentication, rate limiting and logging. The internal endpoint should never be reachable from outside.

Q8

What if I need ad hoc queries?

Send the results to a database or analytical store designed for that. Use Interactive Queries for the narrow, fast lookups.

Q9

How many standby replicas should I use?

One is a common starting point. More copies increase resilience and read options but cost disk, network and processing. Decide from your recovery-time goals.

Q10

Can I change a store name later?

Renaming changes the changelog topic name, so the application will normally start with an empty store and rebuild from the input. Plan names carefully at the start.

Q11

Why do some of my lookups return nothing while others work?

Most likely you read the local store without checking ownership. Only the owner of a key’s partition can answer for it. Use discovery, or check for a mismatched key serializer.

Q12

Is IQv2 ready to use?

It has been introduced in the 3.x line and continues to evolve. Read the release notes for your version, and keep your query code behind an internal interface so it is easy to adapt.

SSummary and Key Takeaways

The whole guide in a few lines.

Kafka topics store events, and they cannot be queried for current answers. Kafka Streams keeps running results in local state stores, backed up by changelog topics. Interactive Queries let your own code read those stores, read-only, so that a stream job can answer questions without a separate database. Because state is divided among instances, every request needs to find the owner of its key, read locally or forward once, and cope with instances that are starting, restoring or changing owners. You provide the network layer, the security, the retry behaviour and the monitoring. Used for narrow key lookups with realistic expectations of freshness, the approach removes moving parts. Used as a general database, it disappoints.

Key Takeaways

  • Only named state stores in a Kafka Streams application are queryable; plain topics are not.
  • Each instance holds only its share of the data, so find the owner of every key and forward at most once.
  • Kafka Streams gives discovery, but you build, secure and monitor the network layer.
  • Queries succeed only in the RUNNING state; use readiness gating, retries with backoff, and standby replicas.
  • Answers may be slightly stale or uncommitted; let callers choose their freshness needs.
  • Design keys and precomputed aggregates so common questions are point lookups, and cap or avoid scans.
  • Choose Interactive Queries for simple, fresh lookups; choose a database for rich or heavy querying.