Hi @adhi_databricks !
This is a very specific and subtle architectural edge case when migrating traditional Scala JAR workloads to Databricks Serverless Compute.
The core issue comes down to how Databricks Serverless Jobs execute code via Spark Connect.
Unlike Classic Compute—where your Scala JAR runs directly inside the JVM on the driver node—Serverless Compute executes user code through the Spark Connect architecture (gRPC transport layer).
Here is the breakdown answering your specific questions:
1. Is Scala foreachBatch from a Serverless Jobs JAR officially supported?
While Scala JAR tasks are supported on Serverless, using arbitrary Scala closures within foreachBatch on Spark Connect / Serverless has known limitations.
In Classic Compute, foreachBatch executes a local JVM closure directly on the driver. In Serverless (Spark Connect), foreachBatch requires serializing and streaming function calls and DataFrame operations back and forth over a gRPC channel between the client context and the remote Spark Connect server.
When complex Scala closure state (or compiler-generated synthetic classes) is passed over this gRPC transport layer, it frequently triggers serialization or protocol mismatches, leading to stream termination.
2. Is UDF_ERROR.INTERNAL wrapping RST_STREAM / PROTOCOL_ERROR a known transport issue?
Yes. RST_STREAM with PROTOCOL_ERROR is a lower-level HTTP/2 and gRPC transport error.
It indicates that the gRPC channel between the Spark Connect driver client and the Serverless backend was abruptly closed due to an unhandled serialization failure or channel reset during the execution of the closure.
The UDF_ERROR.INTERNAL is simply a high-level Databricks wrapper error catching the failed RPC invocation. It is not an error in your business code logic, which is why even a no-op foreachBatch fails with the exact same error.
3. Workarounds & Alternatives
Until full parity for native Scala closures over Spark Connect in Serverless is reached, here are the recommended architectural workarounds:
Option A (Recommended for Serverless): Delta Live Tables (DLT) or Declarative Pipelines
If you are running streaming CDC/Kinesis pipelines to Delta with foreachBatch, moving the pipeline to Delta Live Tables (DLT) / Auto Loader with CDC APIs (apply_changes) completely bypasses foreachBatch and runs natively on Serverless without gRPC closure serialization overhead.
Option B: Run as a Classic Compute Job
If foreachBatch with custom Scala logic (complex merges/side-effects) is strictly required for your architecture, running this specific streaming task on a single-node or small Auto-scaling Classic Single-User/Shared Cluster remains the most stable path today.
Option C: Refactor to Pure DataFrame / SQL Writes
If your foreachBatch logic can be expressed purely via native Delta MERGE or DataFrame writes using .writeStream (without custom UDFs or Scala closures inside the batch function), Spark Connect can execute the logical plan natively.
4. Recommended Diagnostics Before Opening a Support Case
If you proceed with opening a Databricks Support ticket (which is recommended to help the product team track this Spark Connect edge case), gather the following:
log4j.logger.org.apache.spark.sql.connect=DEBUG
log4j.logger.io.grpc=DEBUG
- Simplified Standalone Repro: Submit your minimal no-op foreachBatch snippet as a standalone single-class JAR job.
- Driver Log4j Dump: Export the full driver log showing the exact gRPC stack trace right before RST_STREAM.
- Hope this helps clarify why the Delta sink works while foreachBatch fails on Serverless!
If my answer was helpful, please consider marking it as accepted solution!