Apache Spark Structured Streaming with Kafka

Connect Spark Structured Streaming to a self-hosted Kafka cluster using the spark-sql-kafka package, with checkpointed offsets, at-least-once writes and secured SASL_SSL connections. Learn what to plan before the build starts.

On this page

How Spark Structured Streaming reads and writes Kafka

Spark jobs that consume from or publish to a self-hosted Kafka cluster should use the Kafka source and sink provided by the spark-sql-kafka package, connected to the cluster's listeners as they are defined for your environment. That package is the integration path this topic covers, and it runs inside the same security boundary as the brokers, so the job needs network reachability to the bootstrap servers and the credentials your cluster requires. Spark does not hold a copy of the topic configuration or the ACLs. Those stay on the Kafka side, which means the team must confirm early which listener, protocol and SASL mechanism each job is allowed to use.

Spark tracks its own progress through checkpoints. For each query, the checkpoint location records the Kafka offsets that a micro-batch has read and processed, so a restarted job resumes from recorded offsets rather than from consumer group commits. Delivery guarantees are not a property of Spark alone. They depend on the sink and on how the job is designed: a Kafka sink can write records again after a failed batch is retried, so duplicates must be handled through idempotent keys, deduplication logic or a downstream store that tolerates replays. Teams should decide the guarantee they need for each pipeline before writing code, and they should ask whoever owns the platform to confirm how the checkpoint directory is stored and backed up in their infrastructure.

What you need before connecting Spark to Kafka

Connecting Spark Structured Streaming to Kafka touches three systems you usually do not own in the same team: the Spark runtime, the Kafka cluster and the network between them. The cards below cover the decisions and checks to settle with the platform team before the first query runs.

Choose the Spark release line

Spark 4.2.0 was released on July 14, 2026, and 4.1.3 and 4.0.4 followed as maintenance releases on July 15, 2026, with 3.5.9 on July 16, 2026. Spark 4 is pre-built with Scala 2.13 and has dropped Scala 2.12, so any application jars and the spark-sql-kafka package must match that Scala build. Confirm with the platform team which line your organization will support, and how long it expects to patch it.

Run a Kafka 4.x KRaft cluster

Kafka 4.0 and later run only in KRaft mode, so the cluster must not depend on ZooKeeper. Supported releases listed by the project are 4.3.1, 4.2.2 and 4.1.2. Ask the Kafka owners which version is in production and whether the bootstrap servers you will use are reachable from Spark.

Confirm listeners and TLS settings

Spark needs a broker listener that matches the security protocol your clients use, typically SASL_SSL with a SCRAM or OAUTHBEARER mechanism. Ask for the bootstrap address, the advertised listener names and the CA certificate chain, and confirm that broker hostnames in the certificates match what executors resolve. A plaintext listener may exist for internal tools, so do not assume it is the one Spark should use.

Check the Java runtime on every node

Brokers require Java 17, but Spark executors and the driver run on their own JVMs. Kafka clients and Kafka Streams applications need at least Java 11. Confirm the JVM version on each Spark worker, the driver and any container image, and make sure the Kafka client libraries bundled with your Spark distribution match the broker version you are targeting.

Map network paths from executors

Every Spark executor, not only the driver, opens its own connections to the brokers, so the firewall and routing rules must cover all worker nodes or pods. Kafka returns broker addresses from metadata, which means each advertised address must be reachable from every executor. Ask the network team to test from a worker node in each segment before you debug application code.

Decide on the identity source

Choose whether streaming jobs use a dedicated service account with SCRAM or Kerberos credentials, or short-lived delegation tokens issued to the Spark application. Delegation tokens suit long-running jobs that outlive a single credential, but they need a renewal process and a token owner. Ask the Kafka administrators which ACLs the service account needs for the topics, consumer groups and transactional IDs your jobs will use.

Configure the Spark Kafka source and sink in order

This sequence takes Spark from a dependency on the Kafka connector to a streaming query that reads a topic, tracks progress in a checkpoint and writes results to a sink. Each step ends with a check, so a fault is isolated before the next layer is added. Run it against a non-production cluster first.

  1. Add the spark-sql-kafka package for your versions

    The platform engineer adds the spark-sql-kafka package to the application build, choosing the artifact that matches the Spark release and the Scala binary version of the Spark installation. Spark 4 is pre-built with Scala 2.13 and has dropped Scala 2.12 support, so the Scala 2.13 artifact is the one to use on a Spark 4 cluster. Confirm the exact coordinates in the Spark Kafka integration documentation for your release, and deploy the same package to the driver and every executor. Done when the application starts and the Kafka data source resolves without a class-not-found error.

  2. Create the test topic with planned partitions

    The Kafka administrator creates a dedicated test topic using the partition count and replication factor from your capacity plan. Spark creates one read task per partition, so the partition count sets the maximum read parallelism for that topic. Ask the Kafka team to confirm the topic settings and to grant the Spark service identity read and write access through the broker authorization policy. Done when the topic describes with the planned partition count and the Spark identity can both produce and consume test records.

  3. Produce a known set of test records

    Load a fixed, countable set of records into the test topic, for example a few thousand JSON messages spread across several keys (illustrative). Record the expected total and the end offset of each partition before Spark reads anything, so the later counts have a reference. Use the producer tooling your team already runs. Done when the broker reports end offsets whose sum matches the number of records you sent.

  4. Validate connectivity with a batch read

    Run a batch read with spark.read.format('kafka') using the bootstrap servers, the topic name and the security settings for your listener, such as SASL_SSL with the mechanism your Kafka team has issued. Set startingOffsets to earliest and endingOffsets to latest so the read covers all test records. Count the rows and compare the result with the total from the previous step. A failure here points to network reachability, TLS trust or credentials, which is easier to diagnose than inside a running stream. Done when the row count matches the expected total exactly.

  5. Start a streaming read with startingOffsets

    Switch to spark.readStream with the same options and set startingOffsets deliberately. earliest replays the retained history, and latest starts from new records only. Use earliest for the test so every record is seen, and document the choice, because it decides what a first run without a checkpoint processes. Start the query with a console or memory sink and confirm that the batch progress reports the expected input rows. Done when the query shows input rows from the test topic and the Spark UI lists the Kafka source with its offset ranges.

  6. Set the checkpoint location on durable storage

    Set checkpointLocation to a path on storage that survives driver restarts, such as a shared file system or object store your team operates, and give each query its own directory. The checkpoint records source offsets and sink progress, so a restarted query resumes from the last committed batch. Ask the storage team to confirm durability and that the Spark driver can read and write the path. Done when the checkpoint directory contains offset and commit entries after the first micro-batches complete.

  7. Write to the sink and confirm restart behavior

    Write the stream to the target sink in a format or table your downstream team already reads, and attach the same checkpoint location. Add a second set of test records, stop the query and restart it from the checkpoint. Compare the sink contents with the two test totals. Any duplicate rows should be explained by the sink design, which the at-least-once and deduplication guidance covers. Done when the sink holds each test record once, and the checkpoint commit log advances after the restart.

How offsets and checkpoints decide what gets reprocessed

Spark's Kafka source records the offset it has processed for each topic partition inside the query checkpoint, so a restarted query resumes from the recorded position rather than from a fresh read. This makes the checkpoint the authoritative record of progress, and it is the first design decision to settle. The startingOffsets option (earliest, latest, or a JSON map of explicit offsets) only applies when a query starts with no checkpoint. Once a checkpoint exists, startingOffsets is ignored, so operators who expect a config change to rewind or skip data will be surprised. Treat a rewind as a deliberate operation: stop the query, decide whether to start a new checkpoint location, and document which offsets the new run will read.

Changing the checkpoint, the query or the topic can break resumption in ways that are not always obvious. Adding or removing stateful operators, changing aggregation keys, or altering the schema of stored state can make the saved checkpoint incompatible with the new plan, and Spark will fail at startup or require a new checkpoint. Changing the topic name or pointing at a recreated topic means recorded offsets may refer to data that no longer exists. Before any change, confirm the Spark version and query plan compatibility with the Spark Structured Streaming documentation for your release, and rehearse the change against a copy of the checkpoint in a non-production environment.

Offsets expire when Kafka's retention deletes the segments that hold them. If a query falls behind longer than retention, the recorded offset points to data that is gone. Spark's Kafka source can fail the query in this case, and the failOnDataLoss option controls whether that failure stops the query or only logs a warning and skips ahead. Setting it to false trades completeness for availability, so the choice belongs in the design review with the data owners, not in a default. The checkpoint directory must also live on storage that survives pod or node replacement, such as a persistent volume or an object store path that the platform supports for checkpoints. A checkpoint on ephemeral container storage means every reschedule becomes a silent reset to startingOffsets, which can mean reprocessing a full retention window or losing data. Confirm the checkpoint location, its backup and its access rights as part of the runbook.

Choosing write semantics: at-least-once and deduplication

When Spark writes to a Kafka topic through the spark-sql-kafka package, the default guarantee is at-least-once. Spark's micro-batch engine replays a batch from its checkpointed offsets after a failure, and the Kafka sink then sends the rows again. Whether those replays reach the topic as duplicates depends on the producer settings Spark uses, so the producer configuration is part of the write design, not an afterthought.

  • Producer retries: Transient broker errors cause the producer to resend records. Ask whether idempotence is enabled for the sink's producer and confirm the behavior against the version of the spark-sql-kafka package you deploy.
  • Batch replay: If a failure occurs after records are written but before the checkpoint commit, the batch is processed again and its rows are written again.
  • Keys for downstream dedup: Set a stable key on each record so that consumers can collapse repeated values. Kafka keeps records with the same key in one partition, which helps ordering but does not remove duplicates.
  • Consumer side handling: Consumers that need exactly-once results must deduplicate on a business key or use transactional reads, which the Kafka producer and consumer documentation describes.
  • Retention and compaction: A compacted topic keeps the latest value per key, which can hide duplicates from later readers but is not a substitute for idempotent processing.

Before go-live, confirm the producer settings, the key scheme and the consumer-side rule for ignoring repeats with your platform team and the team that owns the downstream topic.

Securing the connection with SASL_SSL and delegation tokens

A Spark job talks to Kafka through the same security layers as any other client: TLS for encryption, a SASL mechanism for identity, and ACLs for what that identity may touch. Delegation tokens matter because streaming jobs run far longer than a typical client session, so the cards below cover each layer and the checks to run with the cluster owner.

TLS encryption in transit

Kafka encrypts traffic between clients, brokers and tools with SSL/TLS, and security is optional, so a cluster can run unsecured or mix secured and unsecured listeners. For Spark, the Kafka source and sink must connect to a SASL_SSL listener, not a plaintext one. Ask the cluster owner for the bootstrap server port that carries SASL_SSL, the CA bundle the Spark executors must trust, and whether the listener requires client certificates. Check that the truststore is distributed to every driver and executor, not only the driver.

Choosing a SASL mechanism

Kafka documents GSSAPI (Kerberos), PLAIN, SCRAM-SHA-256, SCRAM-SHA-512 and OAUTHBEARER for SASL authentication. Spark passes the mechanism through the Kafka client options, so the choice must match what the brokers enable. SCRAM credentials are stored in the cluster and suit service accounts, while OAUTHBEARER fits organizations that already issue tokens from an identity provider. Confirm the enabled mechanisms and the listener name with the cluster owner before writing any job configuration.

Delegation tokens for long-running jobs

A structured streaming query can run for months, and a password or Kerberos ticket may expire while it is still active. Delegation tokens let a job authenticate to Kafka without carrying the main credential into every executor. Confirm with the cluster owner that delegation tokens are enabled, who can create them, their maximum lifetime and how often they must be renewed. Plan the renewal process and test a query that runs past the first token expiry before going to production.

Token renewal and job restarts

A token that cannot renew will stop a query, and the failure may appear only after days of normal operation. Decide who owns renewal, whether the Spark application or the cluster scheduler handles it, and what happens when renewal fails. Test a forced token expiry in a non-production environment and confirm that the query recovers from its checkpoint with the correct offsets. Alert on authentication errors in the driver logs so that a stopped query is noticed quickly.

ACLs for the Spark principal

The Spark principal needs read access to source topics and the consumer group it uses, and write access to sink topics. Each Spark query uses its own consumer group, so ACLs must cover those group names as well as the topics. Resources without ACLs are restricted to super users in KRaft clusters unless allow.everyone.if.no.acl.found is set to true. Ask the cluster owner to confirm the authorizer settings and to grant topic and group rights with the narrowest scope the job needs.

Authorizer settings to confirm

In KRaft clusters, the StandardAuthorizer stores ACLs in the cluster metadata log. Before testing, ask the cluster owner which authorizer is active, whether allow.everyone.if.no.acl.found is set, and whether an external authorization service is in use. A Spark job that works in a test cluster with permissive defaults can fail in production once those settings are tightened. Record the answers with the job configuration so the next change to the cluster does not break the query without warning.

Mapping Spark options to Kafka settings and identities

This table maps each Spark Kafka source and sink option to the client setting, principal and ACL it produces. Read it when you define the identity a Spark job runs under, since the same option can require different permissions on the read and write sides. Confirm the exact behavior for your Spark and Kafka versions in the package documentation before you write the ACLs.

Spark optionKafka effectACL and identity consideration
subscribe or subscribePatternSelects topics explicitly or by regex; the source assigns partitions and tracks offsets for themRead on each topic; with subscribePattern, confirm the principal can describe every matching topic, since new matches appear over time
startingOffsetsSets where a new query begins (earliest, latest or per-partition JSON); it applies only when no checkpoint existsChanging it after a checkpoint exists has no effect; plan replays by new checkpoint location, not by editing this option
kafka.bootstrap.serversSets the initial broker list the client uses to discover the clusterNetwork path and listener must match the security protocol; a plaintext listener cannot serve a SASL_SSL client
kafka.security.protocolSets SASL_SSL, SASL_PLAINTEXT, SSL or PLAINTEXT on the client connectionMust match a listener the broker actually exposes; mismatches typically surface as connection or authentication failures
kafka.sasl.mechanismSelects the SASL mechanism, such as SCRAM-SHA-512, GSSAPI or OAUTHBEARER, from those the cluster enablesThe mechanism determines which principal the broker sees; confirm it with the platform team before wiring credentials
Consumer group (group.id)Spark sets a generated group per query unless configured; offsets are tracked through checkpoints, not committed group offsetsGrant Read on the group resource where the principal needs it; confirm how your Spark version names and isolates groups before writing ACLs
checkpointLocationStores offsets and query progress for recovery; it is not a Kafka settingMust be durable storage the driver can write; a lost or shared checkpoint directory changes which offsets a restart resumes from
Sink topic (kafka topic option)Writes records to the named topic; the sink does not take a consumer groupRequires Write and Describe on the topic; grant Create only if you intend the job to create topics
Transactional identitySpark's Kafka sink does not expose a transactional.id option; confirm current support before planning exactly-once writesDo not assume transactional fencing across restarts; confirm the delivery semantics your Spark version provides

Questions teams ask before connecting Spark to Kafka

What drives the effort and cost of a Spark and Kafka integration?

The biggest drivers are the number of topics and sinks, the message format and schema handling, the amount of stateful logic, and how many security layers the integration must pass through. A job that reads one topic with a simple JSON parse and writes to one table is far smaller than a pipeline with joins, watermarks, deduplication and several sinks. Cost has two parts: one-time design, build and test work, and recurring operations such as upgrades, checkpoint handling and on-call. Ask for a quote that lists each source, sink, offset and checkpoint strategy, the authentication method (for example SASL_SSL with delegation tokens), and what the client's team will own after handover.

How long does a Spark and Kafka integration take?

No duration is given here, because the answer depends on the sources, sinks, security model and how much of the pipeline logic is new. Duration rises with each additional topic, each external sink with its own exactly-once or upsert behavior, and each access control layer that must be reviewed. Ask for a written plan with phases, the environments included (development, test and production), the acceptance tests for offsets and recovery, and the client decisions that must be made before build starts. Any timeline should be tied to a fixed scope agreed in writing.

What does the client's team need to provide?

The work needs access and decisions more than code. Expect to provide the Kafka cluster details and listener endpoints, the topic names and expected message schemas, a named owner for each topic and sink, a Spark cluster or runtime where jobs will run, and a way to store and rotate credentials. Your security team should confirm the authentication mechanism, the TLS trust chain and the ACLs that allow the Spark principal to read and write. Your data team should confirm dedup keys, retention expectations and how late data should be handled.

Can an internal team build this without a partner?

Yes, if the team already runs Spark in production and understands Kafka offsets, consumer groups and checkpoints. The skills that most often decide success are restarting a streaming query from its checkpoint, choosing an at-least-once or idempotent write pattern, and setting up SASL_SSL and delegation tokens correctly in a cluster that the team operates. A partner is most useful when the team is new to one of these areas, when a fixed delivery date matters, or when the team needs an independent review of the design before it goes live. Whichever path you choose, the design decisions should be written down and owned by someone inside your organization.

Which Spark and Kafka versions should we pick?

Pick a Kafka release from the supported list on the Kafka downloads page, and a Spark line that is still maintained, then confirm that the spark-sql-kafka package matches your Spark Scala version. Spark 4 is built with Scala 2.13 and has dropped Scala 2.12, so a Scala 2.12 Spark job will need a migration first. Spark's versioning policy says major and feature branches are maintained for 6 months, and the last release in each major line is an LTS maintained for 18 months. Check the Spark downloads page for current release dates before you choose. Also check that your Java runtime meets the requirements in the Kafka upgrade notes, and test your chosen pair in a non-production environment before going live.

How does delegation token renewal affect long-running Spark jobs?

Delegation tokens expire, so a streaming query that runs for weeks can fail when its token is not renewed. Spark can obtain and renew Kafka delegation tokens for the application, but the renewal interval, the token lifetime set on the Kafka broker, and the principal used must all be configured to match. Confirm with your Kafka administrator how token maximum lifetime and renewal are set, and confirm that the Spark driver can reach the cluster when renewal runs. Test a renewal cycle before production, including a restart from checkpoint, and set alerts on authentication failures so a stalled job is noticed quickly. For the exact configuration keys, check the Spark Kafka integration documentation for your chosen version.

What goes wrong most often with offsets and checkpoints?

The most common failure is changing a query and restarting it from an old checkpoint, which can fail or replay data unexpectedly. Other frequent problems include deleting the checkpoint directory to clear an error, which loses the record of processed offsets, and writing to an external sink without a dedup key, which creates duplicates on retry. Keep checkpoints on durable storage that all restarts can reach, record the starting offsets policy for each new query, and document how to reprocess a range safely. Ask for a restart test as part of acceptance, including a planned upgrade of the Spark version.

How to test the pipeline before production traffic

Test offset recovery before any production traffic arrives, because a Structured Streaming query that restarts from the wrong place either drops records or reprocesses them. Run the query against a test topic, let it commit several micro-batches, stop it, and restart it from the same checkpointLocation. Confirm that the first batch after restart begins at the offset following the last committed one, and that the sink contains each input record exactly once after the sink's own deduplication logic is applied. Then repeat the test after changing the query's startingOffsets value on purpose, to confirm the team understands which setting wins when a checkpoint already exists. Keep a written record of the expected behavior so the on-call engineer has a reference during an incident.

Next, inject failures on the parts of the path that fail in production. Stop a broker or move partition leadership during a run and watch the query's retry behavior and its driver and executor logs. Make the sink unavailable for a short period, for example by blocking its network route or pausing the database, and confirm the query fails cleanly, restarts, and does not advance its checkpoint past uncommitted output. For at-least-once writes, replay a known input window into the source and check that the deduplication key you chose, applied in the sink or in a dropDuplicatesWithWatermark step, produces one output row per business key. Document which failures the query survives without intervention and which need a restart, since that boundary sets your alerting.

Finish with security and capacity checks. Run the query with kafka.security.protocol set to SASL_SSL and confirm that a client without the right credentials, or with a principal that lacks read or write ACLs on the topic and consumer group, is refused with a clear error rather than a silent stall. Check delegation token renewal if you use tokens, including a run that spans the renewal window. For load, run against a topic with the partition count you plan to use in production and measure whether maxOffsetsPerTrigger and the executor count keep batch duration below your trigger interval. Confirm these figures with your own cluster and workload, since partition sizing depends on your data volume and Spark's own resource settings.

How DeployOpen scopes and delivers Spark and Kafka integrations

DeployOpen plans, implements, hardens, documents and supports self-hosted open-source platforms inside your own cloud accounts, data centers or air-gapped networks. For Spark Structured Streaming with Kafka, we start from a written scope covering the Kafka cluster, the Spark runtime, the checkpoint storage and the security settings that connect them.

How we scope

  • Engagements are fixed-scope, with scope, deliverables and timelines agreed in writing before work starts.
  • The written scope lists the Spark version, Kafka cluster, topics, checkpoint storage and sink systems involved.
  • We confirm which SASL mechanism, delegation token setup and TLS settings your Kafka brokers already expose.

How we build

  • We build and test streaming jobs against your Kafka topics, covering offset handling, checkpoint recovery and restart behavior.
  • Write paths are tested for at-least-once delivery and deduplication logic agreed with your data owners.
  • Security testing covers the SASL_SSL configuration and the credentials each job uses to reach Kafka.

How we hand over

  • Deliverables include configuration, job definitions, runbooks for checkpoint recovery and a record of the decisions made.
  • We document how to resume a job from a checkpoint, reset offsets and rotate delegation tokens in your environment.
  • Handover sessions walk your platform and data engineering teams through operating the jobs without DeployOpen.

How we support

  • Ongoing support, upgrades and continuous testing are available as retainers after handover.
  • Retainers can cover Spark and Kafka version upgrades, checked against the release notes for each version you run.
  • Ask any provider to confirm how they test restarts, failover and the upgrade path before agreeing the scope.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.