Skip to main content
This guide runs an open-source Apache Spark cluster across four sandboxes with 32 vCPU each. Every node mounts two Archil disks at the same paths: one backed by an S3 bucket of Parquet files, and one for the Delta Lake tables Spark writes. That gives the cluster three properties:
  • A cluster only while a job runs. A stopped sandbox starts in about a second, so the job script starts the nodes, runs the job, and stops them again. In our runs, the cluster was ready 5 seconds after the script started, and the whole job took about 30 seconds once Archil had cached the data.
  • Your bucket, read in place. Spark reads the bucket’s Parquet files as ordinary files, with no S3 connector or credentials to configure, and Archil caches what it reads. Each node sees the same files at /mnt/overture and /mnt/lake, so a Delta table is a directory of ordinary files that every executor writes to directly.
  • Faster and cheaper than Databricks serverless. In our runs, the cluster finished the same jobs 2 to 5 times sooner than Databricks serverless in performance-optimized mode, for a fifth to a half of the cost, even on a first run with nothing cached.
You’ll aggregate 2.5 billion buildings from the Overture Maps public bucket into a Delta table, query the table, and compare the cluster with Databricks.

Prerequisites

The Overture bucket is in AWS us-west-2, so this guide uses the aws-us-west-2 Archil region. Export an API key created in that region, then install the SDK for the language you’ll orchestrate from. The cluster uses 128 vCPU across four sandboxes, so you need the Developer plan or above; see Plans and limits.
Save each script in this guide in your project directory. Run TypeScript scripts with npx tsx <name>.ts and Python scripts with python <name>.py.

How the nodes connect

Spark’s master, workers, and driver open connections to each other on fixed ports. Sandboxes don’t share a private network, so each node runs wstunnel, an open-source tunnel over WebSockets:
  • A tunnel server on port 9000. The setup script creates a port token for each node’s port 9000, so only the other nodes, which hold that token, can reach it. Nothing else in the sandbox is exposed.
  • A tunnel to every other node. Each node gets an address of its own, 127.0.1.1 for the first node, 127.0.1.2 for the second, and so on. Spark listens only on its node’s address, and on every other node, a tunnel forwards that address’s Spark ports to the node through its port 9000.
Every node reaches every other node’s Spark ports at the same address, so Spark runs unchanged.

Create the disks and the nodes

You need two disks:
  • overture-maps is backed by the public bucket, using the release/2026-09-23.0/ prefix as its root. Its buildings theme is 512 Parquet files, 258 GiB in total. Archil reads them from the bucket as Spark asks for them.
  • spark-lake is a regular Archil disk for the tables Spark writes.
A disk backed by a public bucket is read-only: Archil has no credentials to write to the bucket. The nodes mount it with readOnly: true (read_only=True in Python), and write their output to the lake.
An Overture release never changes, so the setup script marks overture-maps immutable with archil set-immutable. An immutable disk’s files can’t change, so any number of sandboxes can read them at full speed at the same time. Marking a disk takes a writable mount, so the script runs the command once in a small sandbox and deletes it. This script runs once in every node, during setup. It downloads the tunnel and Delta Lake to the node’s root filesystem, so no job waits on a download:
install.sh
This script runs in every node while the cluster is up. It mounts the lake, starts the tunnels, writes Spark’s settings, and runs Spark’s master on the first node and a worker on every node:
node.sh
Each node mounts the lake itself, in conditional mode, so every node can write to it, and with --cache-expiry-secs 1, so each node sees the files other nodes write within a second. Delta Lake needs that: after the driver commits a table, executors on every node read the commit’s files. The script runs four tasks per vCPU, because tasks reading uncached data mostly wait on object storage, not compute. The setup script creates both disks and a token for the lake, then creates the four nodes, each with 32 vCPU and 64 GiB of memory and overture-maps mounted read-only. It gives every node its settings, the other nodes’ tunnel addresses, and the scripts, runs install.sh, and stops the node:
In our runs, the setup script took 30 to 55 seconds. A stopped sandbox doesn’t accrue compute charges, and it keeps its root filesystem, so the next start still has the scripts, the tunnel, and Delta Lake.

Run the job

This is the job. It reads only the four columns it needs from every building, counts the buildings in each 1-degree grid cell, with their average height where it’s known, and saves the result as a Delta table. Spark spreads the files across every executor in the cluster:
buildings.py
The job script starts every node and its part of the cluster, waits for the workers to join the master, submits the job on the first node, and stops every node, so you pay for the nodes only while the job runs:
Spark read only the four columns the job uses, about 26 GiB of the 258 GiB in the bucket, and wrote 16,192 grid cells. Run the script again: the second time, the executors read from Archil’s cache instead of the bucket. Run the job from any scheduler that can run a script, such as cron, GitHub Actions, or Airflow. If the script dies before it stops the nodes, idleTtlSeconds pauses each node after 10 minutes without a connection, which also stops compute charges.

Query the table

The table is an ordinary Delta table, so Spark SQL can query it by path. This query finds the five grid cells with the most buildings:
query.sql
One node is plenty for a query of the finished table. This script starts the first node, runs the query on it, and stops it:
The busiest cells are Jakarta, Tokyo, São Paulo, Central Java, and Kolkata. The script took under 20 seconds, most of it starting the JVMs. Any sandbox, server, or cluster that mounts spark-lake can read the table too.

Running it in production

  • Size the cluster per job. Change NODES in the setup script for more or fewer nodes. Because the nodes run only while a job runs, twice the nodes for half the time costs about the same, so give big jobs more nodes. A sandbox can have up to 32 vCPU and 64 GiB of memory, and node.sh sizes Spark to the sandbox. Spark writes shuffle and spill files to each node’s root filesystem.
  • Mark data immutable once it’s final. Overture publishes each release under its own prefix and never changes it. When you back a disk with your own bucket, give each finished dataset, such as a dated partition, its own immutable disk, and keep data that’s still being written on a regular disk. Objects your other systems add to a bucket can take minutes to appear on a disk; see From the bucket to the disk.
  • Share the lake. Any number of clusters, servers, and sandboxes can mount spark-lake in conditional mode at the same time, and read or write tables there.
  • Keep the tokens to the nodes. The port tokens and the lake token are stored only on the nodes’ root filesystems. Deleting a node revokes its port token, and removing the lake’s token user revokes the lake token; see Managing disks.
  • Pin your versions. The nodes run the official spark image with Spark 4.2.1, Delta Lake 4.4.1, and wstunnel 11.1.0. To start faster and control when they change, build an image with them installed; see Images.

Compared with Databricks serverless

Databricks serverless compute includes the instances in its DBU rate, and Databricks chooses how much compute each job gets, so we measured it. We ran buildings.py as a Databricks serverless job on the Premium tier in AWS us-east-2, reading a copy of the same Overture release from an S3 bucket in that region, in both of its modes. We also ran a second job that reads every column of every building, 252 GB. Each Databricks job’s cost is its DBU usage times the Premium list price of $0.35 per DBU. This cluster's cost is the time its four nodes ran, at Archil's list [sandbox rates](/administration/billing#persistent-sandboxes-and-services), $12.16 an hour for the four nodes. Each time is from start to finish, including startup: for this cluster, starting and stopping every node, submitting the job, and Spark’s startup. A first run reads from the bucket; later runs on this cluster read what Archil cached, while Databricks serverless starts each run on fresh compute: Archil samples running sandboxes once a minute, so a single run this short costs a minute of the nodes or nothing, and the time they ran on average. Against performance-optimized mode, the cluster finished 2 to 5 times sooner, for a fifth to a half of the cost. Against standard mode, it finished 6 to 14 times sooner, for 40% less on the scan of every column. Most of the difference is size: Databricks’ query history shows that serverless ran these jobs 7 to 24 tasks at a time, while this cluster starts 128 vCPU on the job at once. To price your own serverless jobs, add up each job’s rows in the system.billing.usage table, which Databricks fills in over the few hours after a job runs:
Archil bills the disks separately, for active data: the data read in the last hour. For a job like this one that runs every hour, the 26 GiB it reads is about $5 a month; see Pricing & Billing.

Clean up

Delete the nodes and both disks:
Deleting overture-maps doesn’t touch the public bucket.

Next steps

Sandboxes

Create, start, and stop sandboxes, and reach their ports.

Sharing disks

Mount one disk on many sandboxes and servers, and choose between shared and conditional mode.

Analytics with DuckDB

Query the same Overture data with DuckDB, and serve results over HTTPS.

ClickHouse

Run ClickHouse on an Archil disk, and branch the database for heavy queries.