Scaling OCSF Validation in Databricks: From 4GB Disk Spills to Zero

TL;DR: During an OCSF 1.8.0 rollout, a validation stage that should have completed in seconds was taking more than 45 minutes and spilling nearly 4GB to local disk. By redesigning our Pandas UDF execution model and replacing interpreted validation with compiled validators, we reduced spills to zero and improved throughput by up to 20x.

The Problem That Started the Investigation

While upgrading our cybersecurity data platform to support OCSF 1.8.0, we noticed validation jobs taking far longer than expected. The workload itself wasn't particularly large, which made the Spark UI metrics confusing. Executors were spilling gigabytes of data to disk, task durations were increasing, and Photon utilization remained surprisingly low.

At first glance, the symptoms looked like a cluster sizing issue. However, increasing resources produced only marginal improvements. The real bottleneck was hidden inside the Python execution layer.

Why OCSF Validation Is Different

Most Spark pipelines operate on a fixed schema. OCSF validation introduces a different challenge. Every incoming event contains a class_uid that determines which schema should be used for validation.

  • Authentication events use one schema.
  • Network activity events use another.
  • Application activity events use yet another.

This means validation cannot rely on a single Spark StructType. The validation framework must dynamically resolve the correct schema at runtime and validate each record accordingly.

Original Architecture

The original implementation was straightforward. Spark records were passed into a Pandas UDF, converted into JSON, and validated using Draft7Validator from the jsonschema library.

What We Found

1. The JSON Expansion Tax

Spark stores data efficiently in compressed columnar formats such as Delta Lake and Parquet. Once those records cross the JVM-Python boundary and become JSON strings, memory consumption increases dramatically.

What looked small inside Spark became significantly larger inside Python workers.

2. The Batch Size Trap

Arrow batches containing thousands of records were being transferred into Python workers simultaneously. Each worker had to deserialize, expand, and validate large numbers of JSON documents before returning results.

As memory pressure increased, Spark began spilling data to local storage.

3. Interpreter Overhead

The jsonschema library is flexible and standards-compliant, but it evaluates schema rules repeatedly. Even though the schema definition remains unchanged, the validation engine spends CPU cycles interpreting the same rules again and again.

Fix #1: Iterator-Based Processing

Insert Image: the-solution.png

The first objective was eliminating memory pressure. Instead of processing an entire Arrow batch as a single unit, we switched to an Iterator Pandas UDF and introduced internal micro-batching.

for batch in iterator:
    for chunk in split(batch, 1000):
        yield validate(chunk)

This change did not alter Spark partition sizes. Instead, it changed how memory was consumed inside Python workers.

By processing smaller chunks and immediately yielding results downstream, the garbage collector could reclaim memory before the next chunk arrived.

Impact

  • Disk spill reduced from ~4GB to 0 bytes.
  • Memory consumption became predictable.
  • Executor stability improved significantly.

Fix #2: Compiled Validation

Once memory pressure disappeared, CPU utilization became the next bottleneck.

Insert Image: compiled-bytecode-engine.png

We replaced jsonschema with fastjsonschema and introduced executor-level caching.

@lru_cache(maxsize=None)
def get_validator(schema_path):
    return fastjsonschema.compile(load_schema(schema_path))

Instead of interpreting rules repeatedly, validators are compiled once and reused for subsequent records.

The validation path becomes a direct function invocation rather than a schema interpretation cycle.

Production Results

Metric Before After
Task Duration 1.42 Minutes 30.49 Seconds
Disk Spill ~4GB 0 Bytes
Photon Utilization 3% 16%
Throughput Baseline Up to 20x Faster

Lessons Learned

The biggest lesson was that Spark itself was not the bottleneck. The bottlenecks were hidden inside Python worker execution.

  • Serialization costs matter.
  • Iterator UDFs are powerful for memory-sensitive workloads.
  • Compiled validators can dramatically reduce CPU overhead.
  • Disk spill metrics often reveal issues before CPU metrics do.

Final Thoughts

By combining iterator-based processing with compiled schema validation, we transformed a spill-heavy validation pipeline into a stable and scalable platform component. The solution reduced infrastructure waste, improved throughput, and provided a foundation for future OCSF adoption at scale.

Data Platform Engineering · Databricks · Data Quality