​
What we’re building

Three sandboxes sharing a single SQLite database through a Chalk volume:

  1. A writer sandbox creates the database, inserts rows, and terminates.
  2. Two reader sandboxes start afterward, mount the same volume, and independently query the data.

This shows that data written to a volume outlives the sandbox that created it, and that multiple sandboxes can read it concurrently. The pattern is useful for experiment tracking, lightweight coordination state, or any case where you want persistence without standing up a database server.


​
Shared configuration

All three sandboxes use the same slim Python image. SQLite ships with Python’s standard library, so no extra packages are needed.

import chalkcompute

VOLUME_NAME = "sqlite-persistence"
MOUNT_PATH = "/data"
DB_PATH = f"{MOUNT_PATH}/app.db"

image = chalkcompute.Image.debian_slim("3.12")

volume = chalkcompute.Volume(VOLUME_NAME)
volume.put_file("README.txt", b"Shared SQLite database at /data/app.db\n")

The volume is the only piece of shared state. Each sandbox mounts it at /data and accesses the database at /data/app.db.

A sandbox can mount only a volume that already exists. chalkcompute.Volume(VOLUME_NAME) does not contact Chalk by itself; the volume is created the first time you read or write through it, here when put_file uploads a README.


​
Step 1: Write the database

The writer sandbox creates a users table and inserts three rows. After the script finishes, the sandbox is terminated, and the data now lives only on the volume.

writer = chalkcompute.Sandbox(
    image=image,
    name="sqlite-writer",
    volumes=[(VOLUME_NAME, MOUNT_PATH)],
).run()

result = writer.exec("python3", "-c", """
import sqlite3, os

db_path = "/data/app.db"
os.makedirs(os.path.dirname(db_path), exist_ok=True)

conn = sqlite3.connect(db_path)
conn.execute(
    "CREATE TABLE IF NOT EXISTS users "
    "(id INTEGER PRIMARY KEY, name TEXT, email TEXT)"
)
conn.execute("INSERT OR IGNORE INTO users VALUES (1, 'Alice', 'alice@example.com')")
conn.execute("INSERT OR IGNORE INTO users VALUES (2, 'Bob', 'bob@example.com')")
conn.execute("INSERT OR IGNORE INTO users VALUES (3, 'Charlie', 'charlie@example.com')")
conn.commit()

rows = conn.execute("SELECT * FROM users ORDER BY id").fetchall()
conn.close()

print(f"Wrote {len(rows)} rows to {db_path}")
for r in rows:
    print(f"  {r}")
""")

print(result.stdout_text)
# Wrote 3 rows to /data/app.db
#   (1, 'Alice', 'alice@example.com')
#   (2, 'Bob', 'bob@example.com')
#   (3, 'Charlie', 'charlie@example.com')

writer.terminate()

At this point the writer is gone. The volume still holds app.db.


​
Step 2: Read from independent sandboxes

Start two new sandboxes that mount the same volume. Each opens the database in read-only mode and queries it independently, which shows that the data survived the writer’s shutdown.

reader_a = chalkcompute.Sandbox(
    image=image,
    name="sqlite-reader-a",
    volumes=[(VOLUME_NAME, MOUNT_PATH)],
).run()

reader_b = chalkcompute.Sandbox(
    image=image,
    name="sqlite-reader-b",
    volumes=[(VOLUME_NAME, MOUNT_PATH)],
).run()

READ_SCRIPT = """
import sqlite3

conn = sqlite3.connect("file:/data/app.db?mode=ro", uri=True)
rows = conn.execute("SELECT * FROM users ORDER BY id").fetchall()
conn.close()

for r in rows:
    print(r)
print(f"Read {len(rows)} rows")
"""

for name, reader in [("reader-a", reader_a), ("reader-b", reader_b)]:
    result = reader.exec("python3", "-c", READ_SCRIPT)
    print(f"--- {name} ---")
    print(result.stdout_text)
--- reader-a ---
(1, 'Alice', 'alice@example.com')
(2, 'Bob', 'bob@example.com')
(3, 'Charlie', 'charlie@example.com')
Read 3 rows

--- reader-b ---
(1, 'Alice', 'alice@example.com')
(2, 'Bob', 'bob@example.com')
(3, 'Charlie', 'charlie@example.com')
Read 3 rows

Both readers see all three rows. The volume acts as durable shared storage that any number of sandboxes can mount.


​
Cleanup

Terminate the readers. If you no longer need the data, delete the volume:

reader_a.terminate()
reader_b.terminate()

volume.delete()

​
How it fits together

  Writer                  Volume                   Reader A        Reader B
    │                       │                         │               │
    │  sqlite3.connect()    │                         │               │
    │  CREATE TABLE users   │                         │               │
    │  INSERT 3 rows        │                         │               │
    │  conn.commit()        │                         │               │
    │──────────────────────▸│  app.db                 │               │
    │                       │                         │               │
    │  terminate()          │                         │               │
    ╳                       │                         │               │
                            │                         │               │
                            │  volumes=[...]          │               │
                            │◂────────────────────────│               │
                            │  volumes=[...]          │               │
                            │◂────────────────────────────────────────│
                            │                         │               │
                            │  SELECT * FROM users    │               │
                            │◂────────────────────────│               │
                            │  3 rows ───────────────▸│               │
                            │                         │               │
                            │  SELECT * FROM users    │               │
                            │◂────────────────────────────────────────│
                            │  3 rows ───────────────────────────────▸│
                            │                         │               │

​
When to use this pattern

Good fit:

  • Experiment metadata, run configs, or coordination state under ~1 GB
  • Data that one process writes and many read (write-once, read-many)
  • Situations where standing up Postgres or Redis is overkill

Not a good fit:

  • Concurrent writers: SQLite uses file-level locking, so only one writer can hold the lock at a time. For multi-writer workloads, use Postgres.
  • Datasets larger than a few GB: SQLite performs well up to moderate sizes, but a dedicated database handles large-scale analytics better.
  • Low-latency reads under load: volume I/O goes through ChalkFS (FUSE), which adds a small overhead compared to local disk. If you need sub-millisecond reads, consider Redis or an in-memory cache.