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.
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.
Topic
A named channel of events, such as orders or payments.
Partition
One ordered slice of a topic. All messages with the same key land in the same partition.
Key
The field that decides the partition, for example a customer id.
Broker
A Kafka server that stores partitions and serves requests.
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.
| Question | Easy on a topic? | Why |
|---|---|---|
| What happened, in order? | Yes | That is exactly what a log records |
| What is the latest event for key 42? | No | You must scan back from the end of one partition |
| What is the running total for key 42? | No | You must read and add every event for that key |
| Which keys have a total above 1,000? | No | You 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
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.
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.
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"]
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.
| Part | Where it lives | Purpose |
|---|---|---|
| State store | Local disk and memory of an instance | Fast reads and writes for processing |
| Changelog topic | Kafka brokers, replicated | Durable backup for rebuilding the store |
| Record cache | Memory of an instance | Batches updates to reduce writes and downstream traffic |
| Standby replica (optional) | Another instance’s disk | A 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.
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.
Part of the Streams library
It runs inside your application process. There is no separate query server to install.
A query feature of Kafka itself
Brokers and topics cannot be queried this way. Only state built by a Kafka Streams application can.
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.
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.
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
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.
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.
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.
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.
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.
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
A handle for queries
Outside code asks for the store by name and by type, and receives a read-only view.
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.
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.
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
| Operation | Result | Store type |
|---|---|---|
| Count or aggregate by key | A table of running results | Key-value store |
| Aggregate within time windows | A value per key per window | Window store |
| Aggregate within activity sessions | A value per key per session | Session store |
| Read a topic as a table | The latest value per key | Key-value store |
| Read a topic as a fully replicated table | A full copy on every instance | Key-value store, global |
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 type | Shape of the data | Questions it answers well |
|---|---|---|
| Key-value store | One current value per key | Get one key, get many keys, scan a range or a prefix, scan everything |
| Timestamped key-value store | Value plus the time it was last updated | The above, plus “how fresh is this value?” |
| Window store | One value per key per time window | All windows for a key in a time range, or all keys in a window |
| Session store | One value per key per activity session | All sessions for a key |
| Versioned key-value store | Several 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.
Point lookup
One partition, one instance, one read. Suitable for user-facing request paths.
Range or prefix scan in one partition
Efficient if the keys are designed so that related items sit together in the same partition.
Scan across all partitions
Fans out to every instance. Fine for occasional tools, risky on a hot request path.
Time-travel lookups
Versioned stores answer questions about past values without replaying the log.
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.
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.
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.
Look up the key
Call get. You receive the current value, or nothing if the key has never been seen or was deleted.
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
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.
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 information | What it tells you | Typical use |
|---|---|---|
| Active host | The instance that processes this partition and holds the freshest store | Default target for a query |
| Standby hosts | Instances that keep a warm copy of the same partition | Fallback when the active host is unavailable |
| Partition | The partition that the key maps to | Logging, and per-partition lag checks |
| All-metadata listing | Every instance and the store partitions each one holds | Fan-out for scans and status pages |
Two ways to get it wrong
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.
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.
application.server
A host and port each instance advertises. Must be reachable by other instances and by whoever forwards requests.
Metadata for a key
Returns the active host, the standby hosts and the partition for one key.
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.
Public endpoint
Accepts requests such as “get total for customer 42”, finds the owner, and returns the answer.
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
| Option | Strengths | Watch out for |
|---|---|---|
| REST over HTTP | Simple, easy to debug, works with every client | Text encoding cost, less strict contracts |
| gRPC | Compact, fast, strongly typed contracts | More tooling and learning curve |
| Client-side routing | Client asks a metadata endpoint, then talks directly to the owner | Clients must handle changes in ownership |
| Server-side forwarding | Clients are simple and see one address | Extra hop, and each instance needs client code for its peers |
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
Client sends the request
The load balancer chooses any healthy instance. The client neither knows nor cares which.
Instance checks its own readiness
If its Streams state is not running, it should refuse quickly with a “try again” response.
Instance asks who owns the key
This is a local computation over cached group metadata, so it is very cheap.
Local or remote
If the owner is this instance, read locally. Otherwise call the owner’s internal endpoint.
Owner answers from its store
It confirms it still owns the partition, reads the value, and replies.
Result returned
The first instance relays the answer, or a clear error such as “not ready” or “moved, retry”.
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.
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
| State | Meaning | Queries |
|---|---|---|
| CREATED | Object built but not started | Refused |
| REBALANCING | Joining the group, receiving tasks, restoring stores | Usually refused, or possibly stale if allowed |
| RUNNING | Processing normally | Allowed |
| PENDING_SHUTDOWN, NOT_RUNNING | Stopping or stopped | Refused |
| PENDING_ERROR, ERROR | Fatal failure | Refused; the instance must be restarted |
The errors you will meet
Streams not started
The store was requested before the instance reached a state where queries work.
Store migrated
The partition was reassigned to another instance since you obtained the store handle. Discover again and retry.
Store not available
The store exists but is not currently hosted or ready on this instance, for example while it restores.
Unknown store
The name does not exist in the topology. This is a programming mistake, not a transient condition.
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.
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.
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"]
What a good retry policy looks like
Recognise transient errors
Not started, migrated and not available are all temporary. An unknown store is not.
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.
Discover again before retrying
The owner may have changed. Ask the library again rather than repeating the old target.
Give up with a clear message
After a small bounded number of tries, return a “temporarily unavailable” response rather than hanging.
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"]
| Setting or concept | What it does |
|---|---|
| num.standby.replicas | How many standby copies of each store to keep |
| Allow stale stores (query option) | Lets queries read stores that are restoring or standby |
| Local lag information | Reports how far each local store partition is behind its changelog |
| acceptable.recovery.lag | How close a copy must be before it counts as caught up for assignment purposes |
| max.warmup.replicas | How many extra copies may be built in the background while tasks move |
| probing.rebalance.interval.ms | How often the group checks whether warm-up copies have caught up |
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.
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.
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.
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.
Rewinds after a crash
Under some processing settings, a restart may make a value go back to an older state before catching up.
No snapshot across partitions
Two keys read from different partitions reflect different moments. Sums across keys are approximate.
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 case | Tolerates staleness? | Advice |
|---|---|---|
| Live dashboard of counts | Yes, seconds are fine | Allow stale reads, show a “last updated” time |
| Customer-facing order status | Mostly | Prefer active hosts, retry briefly on migration |
| Fraud check before approval | Rarely | Use the active host only, fail rather than guess |
| Financial balance shown as authoritative | No | Query the system of record instead |
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.
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 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.
Per-partition results
Each reply says which partition it came from and how far that store had progressed.
Custom query types
Store authors can support their own kinds of queries without changing the library.
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.
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.
| Aspect | Regular table (KTable) | Global table (GlobalKTable) |
|---|---|---|
| Data on each instance | Only its assigned partitions | All partitions |
| Lookup of any key locally | No | Yes |
| Storage cost | Total size, divided among instances | Total size, on every instance |
| Fits huge data sets | Yes | Only if the data fits on one machine |
| Best for | Large, partitioned state | Small, slow-changing reference data |
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.
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.
List the hosts of the store
Ask the library for all instances that hold any partitions of this store.
Send each a local-only scan
Each instance scans just its own partitions and replies with a bounded result.
Merge the partial results
Combine, sort if needed, and apply the overall limit.
Tolerate missing shares
Decide up front whether one unreachable instance fails the whole request or returns a partial answer marked as such.
| Scan type | Reach | Guidance |
|---|---|---|
| Prefix scan with a key that includes the partition key | One partition | Good on request paths |
| Range scan on partition key | Possibly many partitions | Fan out, limit the size |
| Full scan | Every partition | Admin tools and reports only |
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.
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.
Fixed or sliding time slices
Ask for one key across a time range and you get one value per window that overlaps it.
Activity bursts
Ask for one key and receive each session with its start, end and value.
Old windows disappear
Windows are kept for a configured retention period, then removed. Old data may no longer be queryable.
Late events still update
A window can still change while late events are accepted, so a recent window’s value may grow.
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
Always bound the time range
Asking for a key’s entire history from the beginning of time is a scan in disguise.
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.
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.
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.
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.
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.
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.
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.
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.
| Factor | Effect | What to do |
|---|---|---|
| Local point lookup | Very fast, often well under a millisecond when data is cached | Keep hot keys in the block cache with enough memory |
| Extra network hop | Adds a round trip per forwarded request | Use keep-alive connections and connection pools |
| Serialization | CPU cost of turning bytes into objects and back | Use compact formats, avoid converting large values needlessly |
| Disk reads in RocksDB | Slower when data is not in memory | Size memory for the working set, use fast disks |
| Competing with processing | Queries and stream work share threads, disk and memory | Limit query concurrency, use separate thread pools |
| Restoration in progress | Store is not usable, disks are busy | Standby replicas, faster disks, readiness gating |
Ways to add capacity
Spread the partitions
Up to the number of tasks, more instances means each holds less data and serves fewer keys.
Plan ahead
Adding partitions later changes which partition each key maps to, and requires rebuilding state. Choose a generous number early.
Cache popular answers
A short-lived cache in front of the endpoint absorbs repeated requests for the same key.
Read replicas
Where staleness is acceptable, allow standby copies to serve reads and spread load.
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.
| Question | Interactive Queries | External database fed from Kafka |
|---|---|---|
| Extra systems to run | None beyond the Streams app | A database, and a connector or writer |
| Freshness | Very high, straight from processing | Delayed by the extra hop |
| Query flexibility | Lookups and ranges over keys you designed | Rich queries, indexes and joins |
| Independent read scaling | No, shares instances with processing | Yes, scale the database separately |
| Availability during deployments | Depends on your readiness and standbys | Database stays up while the job restarts |
| Client access | You build and secure the service | Standard drivers and tools |
| Consistency story | Reads may see uncommitted or moving state | Reads 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
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.
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.
Authenticate callers
Require a token or certificate on the public endpoint, and check who is asking on every request.
Authorize each lookup
A user may see their own record, not another’s. Enforce that in the service, not in the client.
Avoid exposing sensitive fields
Return only what is needed. A store may hold personal or confidential values that the API should mask or omit.
Encrypt everything
Use TLS on both the public endpoint and the instance-to-instance internal one.
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.
Limit and monitor use
Apply rate limits and size caps, and log access, so one caller cannot scrape or overload the store.
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.
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.
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.
| Area | What to watch | Why it matters |
|---|---|---|
| Instance state | Current state and number of rebalances | Queries fail outside RUNNING; frequent rebalances mean unstable service |
| Store metrics | Get and range request rates and latencies per store | Shows load and slow reads |
| Restoration | Restore progress, records restored, time spent | Explains long unavailability |
| Lag | Consumer lag and local store lag | Tells how stale answers may be |
| End-to-end latency | Time from event creation to being reflected | Measures freshness for users |
| RocksDB internals | Cache hit ratio, pending compactions, memory (more detailed metrics are usually off by default) | Predicts disk pressure and slow reads |
| Your service | Requests, errors, retries, forwarded hops, timeouts | Kafka Streams cannot see these for you |
Application-level signals to add
Local versus forwarded
The ratio shows how much load goes over the network and whether routing improvements would help.
Retries and give-ups
Rising numbers reveal rebalances or unstable instances.
Latency by path
Track local reads and forwarded reads separately to see where time goes.
Open iterators
A number that only grows means iterators are not being closed.
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.
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.
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
Materialized view
The stream job maintains a ready-made answer per key. Queries only read it.
Single-hop forwarding
Public endpoint discovers and forwards once. Internal endpoint answers locally only.
CQRS style split
Writes go through Kafka as events. Reads come from views built by the stream job.
Readiness gating
Traffic is sent only to instances in the RUNNING state.
Precomputed aggregates
Top-N lists and totals are stored as their own keys, so no scan is needed.
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"]
Anti-patterns to avoid
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.
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.
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.
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.
Key becomes bytes
A serializer converts the customer id to bytes. A different serializer, for example text versus number, gives different bytes.
Bytes become a hash
A fixed hash function turns the bytes into a large number.
Hash becomes a partition
The number is reduced to a value from zero up to the partition count minus one.
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.
| Change | Effect on queries |
|---|---|
| Add partitions to the input topic | Keys map to different partitions, so existing state no longer lines up |
| Change the key serializer | Different bytes, different partition, wrong owner computed |
| Change the grouping key in the topology | The store is keyed differently, so queries must use the new key |
| Change the partitioner on the producer | Discovery must use the same custom rule |
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.
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"]
What makes it faster
Reuse local state
A restarted instance that finds its old files needs only a short catch-up.
Stay nearly ready
A standby copy is constantly catching up, so promotion finishes almost at once.
Smaller changelogs
Changelog topics are compacted, keeping the latest value per key, so a full replay is shorter than the total history.
Fast disks and network
Restoration is bulk reading and writing, so throughput of storage and network sets the speed.
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.
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.
| Resource | Who uses it | Sizing advice |
|---|---|---|
| Heap memory | Your service, Streams caches, serialization | Leave room for bursts of concurrent queries |
| Off-heap memory | RocksDB tables and block cache | Bound it explicitly, since each store can otherwise grow its own share |
| Disk space | Active stores, standby stores, changelog restore data | Plan for active plus standby, plus compaction headroom |
| Disk speed | Reads on cache misses, compaction, restoration | Prefer fast local storage |
| Processing threads | Stream tasks | Match threads to cores and tasks |
| Query threads | Web server workers and forwarding clients | Use a separate pool so queries cannot starve processing |
A simple sizing walk
Estimate the data
Number of keys multiplied by the average size of key plus value gives the raw store size.
Add overhead
Stores use more space than raw data because of indexes and compaction. Allow a healthy margin.
Multiply by copies
Count the active copy plus each standby, and spread across instances.
Estimate the hot set
The portion of data read often should fit in the block cache, so most reads stay in memory.
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.
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.
Add optional fields
Old data lacks the field, and new code treats it as missing. This is generally safe with schema-aware formats.
Rename or retype fields
Old bytes no longer match new code. Plan a migration rather than a quiet change.
Separate store type from API type
Map store values to a stable response format, so internal changes do not break clients.
Use a schema registry
A registry with compatibility rules blocks changes that would break readers.
Upgrading the application
Read the upgrade notes
Kafka Streams versions sometimes change internal formats or metrics. Follow the documented upgrade path for your versions.
Upgrade one instance at a time
Watch the group settle and queries succeed before the next one.
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.
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.
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.
| Technique | Benefit | Cost |
|---|---|---|
| Short-lived answer cache | Fewer store reads and hops for popular keys | Answers may be a few seconds older |
| Request coalescing | Many identical concurrent requests share one read | Slightly more code and care with errors |
| Rate limiting | Protects processing from floods | Some callers see refusals |
| Timeouts on forwarded calls | One slow instance cannot stall all callers | Occasional retries |
| Circuit breaker on a peer | Stops hammering an unhealthy instance | Needs tuning to avoid false trips |
| Bulkheads (separate pools) | Queries and internal calls cannot starve each other | More settings to manage |
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
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.
| Term | Plain meaning |
|---|---|
| Topic | A named stream of events in Kafka |
| Partition | One ordered slice of a topic, holding all events for certain keys |
| Instance | One running copy of a Streams application |
| Task | The unit of work that handles one partition of input |
| Topology | The plan of processing steps in a Streams application |
| State store | A small local database that holds a task’s running results |
| Changelog topic | A Kafka topic that backs up every change to a store |
| Restoration | Rebuilding a store by replaying its changelog |
| Materialized | A setting that names a store so that it can be queried |
| Interactive Queries | Read-only access to state stores from your own code |
| Active copy | The store copy held by the instance currently processing the task |
| Standby replica | A warm spare copy kept on another instance |
| Lag | How far a copy is behind the latest changelog record |
| Rebalance | Reassigning tasks among instances |
| Discovery | Finding which instance owns a key |
| Forwarding | Passing a request to the instance that owns the key |
| Fan-out | Sending one request to many instances and merging the answers |
| GlobalKTable | A table fully copied to every instance |
| Window | A slice of time used for grouping results |
| Session | A burst of activity ended by a gap of silence |
| Readiness | Whether an instance is currently able to answer requests |
| Stale read | An answer that may be slightly behind the latest state |
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.
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.
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.
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.
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.
| Option | You build | You operate | Query power |
|---|---|---|---|
| Interactive Queries | Query service, routing, security | Only the Streams application | Key lookups and simple ranges |
| ksqlDB pull queries | Statements and a client | A ksqlDB cluster | Lookups on tables, with limits |
| Connect sink to database | Connector configuration | Connect and the database | Whatever the database offers |
| Cache written by the job | Writer code | The cache system | Key lookups |
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
List the questions
If they are all “give me the value for this key”, any option works and the simplest wins.
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.
Check the availability need
If reads must survive redeploys of the job, prefer a separate store that stays up on its own.
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.
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
Kill an instance under load
Watch how long queries fail and whether retries hide it from users.
Do a rolling restart
Confirm readiness gating and check the length of any error window.
Add and remove an instance
Confirm ownership changes are noticed and forwarded requests still succeed.
Test with realistic store size
Restoration time on a full-size store is the number that surprises teams most.
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.
| Myth | Reality |
|---|---|
| “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. |
CContainer Deployment and Rolling Restarts
Stateful services are harder to run than stateless ones. A few platform choices make a big difference.
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.
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.
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.
Close Streams cleanly
Handle the stop signal, close the Streams object, and allow enough grace time for it to finish.
One at a time
Replace one instance, wait until it reports ready, then move to the next.
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.
| Setup | Behaviour on restart |
|---|---|
| Empty disk, no standbys | Full restoration, long unavailability for the tasks it owns |
| Persistent disk, no standbys | Short catch-up from the last position, but tasks are still offline meanwhile |
| Standby replicas, any disk | Standbys are promoted quickly, a short pause only |
| Persistent disk, standbys and static membership | Smoothest: few tasks move, quick recovery |
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.
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.
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.
Test the query service logic
Use fake discovery results to check local reads, forwarding, retry and error mapping without a cluster.
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.
Chaos test
Stop and start instances during queries. Confirm bounded errors, recovery and no forwarding loops.
Load test
Measure latency and stream lag together under realistic query rates.
| Test level | Catches | Does not catch |
|---|---|---|
| Topology unit test | Wrong aggregation, wrong keys, wrong store names | Ownership, network, rebalances |
| Service logic test | Retry mistakes, error mapping, loops | Real timing and load |
| Multi-instance integration | Discovery and forwarding errors, wrong serializers | Production-scale restoration |
| Chaos and load | Recovery windows, resource contention | Rare data-dependent bugs |
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.
Choose the key
Orders are keyed by customer id, so all of one customer’s orders share a partition.
Build and name the aggregate
Group orders by customer and sum the amounts into a named store, “customer-totals”.
Configure the instances
Twelve partitions, three instances, one standby replica each, a reachable advertised address, and persistent disks.
Build the service
A public endpoint that discovers and forwards once, and an internal endpoint that answers locally only.
Secure it
Require login, allow customers to read only their own total, use TLS, and keep the internal endpoint private.
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.
Watch it
Track state, restoration, lag, request latency, forward ratio and retries, with alerts on sustained error rates.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.