File Locking¶
How notebook file locking prevents conflicts in multi-user environments.
Overview¶
When multiple users have access to the same Dango project, two people could try to edit the same notebook at the same time. Dango prevents this with file-level locks — time-limited, heartbeat-refreshed locks that ensure only one user edits a notebook at a time.
Locks are:
- Automatic — acquired when you open a notebook, released when you close it (see Getting Started)
- Time-limited — expire after 15 minutes without a heartbeat
- Per-notebook — each notebook has its own independent lock
Locking works alongside DuckDB snapshots — snapshots handle database isolation, while locks handle file editing isolation.
How Locking Works¶
- User opens a notebook — Dango acquires a lock in the
notebook_locksdatabase table - Lock is granted — the lock expires in 15 minutes unless refreshed
- Heartbeat runs — a background thread (CLI) or API call (Web UI) refreshes the lock every 60 seconds
- Each heartbeat resets the expiry to 15 minutes from now
- User closes the notebook — Dango releases the lock immediately
- If the user disappears (crash, network loss) — the lock expires after the last heartbeat + 15 minutes
Heartbeat Mechanism¶
| Parameter | Value |
|---|---|
| Lock duration | 15 minutes |
| Heartbeat interval | 60 seconds |
| Stale lock timeout | 900 seconds (15 minutes since last heartbeat) |
When you run dango notebook open, a background thread sends heartbeats every 60 seconds:
✓ Marimo started (PID 12345)
Open in browser: http://localhost:7805/?file=my_analysis.py
Press Ctrl+C to release lock and exit.
The heartbeat thread runs until you press Ctrl+C or the process is killed.
The browser sends heartbeat requests to POST /api/notebooks/{name}/heartbeat every 60 seconds while the notebook is open. If the browser tab is closed, heartbeats stop and the lock expires.
Stale Lock Recovery¶
Stale locks are cleaned up automatically. Every lock operation (acquire, release, refresh) first deletes any locks whose expires_at timestamp has passed. Additionally, locks whose last heartbeat exceeds 900 seconds (15 minutes) are proactively expired.
This means:
- If a CLI session crashes, the lock expires within 15 minutes
- If a browser tab is closed without releasing, the lock expires within 15 minutes
- No manual intervention is needed in most cases
Locked Notebook Behavior¶
When you try to open a notebook that's locked by another user:
Error: Notebook 'my_analysis' is locked by [email protected].
You'll need to wait for the lock to be released or ask the lock holder to release it.
The notebook shows a lock icon with the lock holder's name and expiry time. You have two options:
- Wait — the lock will expire if the holder is inactive
- Make a Copy — creates a copy of the notebook (e.g.,
my_analysis_copy_20260515_143022.py) that you can edit independently
Force Unlock¶
Users with the notebooks.manage permission can force-release a lock regardless of who holds it.
Warning
Force-unlocking a notebook while someone is actively editing it can cause them to lose unsaved work. Marimo auto-saves frequently, but any in-progress cell edits may be lost.
Permissions¶
Notebook operations require specific permissions:
| Permission | Actions | Default Roles |
|---|---|---|
notebooks.view | List notebooks, view metadata | All roles |
notebooks.execute | Create, open, edit, delete (own), lock/unlock | Editor, Admin |
notebooks.manage | Force unlock, delete others' notebooks | Editor, Admin |
See Permissions Reference for the full permissions list.
Idle Auto-Shutdown¶
When no notebooks have active locks for a sustained period, the Marimo server shuts down automatically to free resources.
| Parameter | Value |
|---|---|
| Idle timeout | 2 hours |
| Warning | 5 minutes before shutdown (via WebSocket) |
| Check interval | Every 5 minutes |
| Parameter | Value |
|---|---|
| Idle timeout | 1 hour |
| Warning | 5 minutes before shutdown (via WebSocket) |
| Check interval | Every 5 minutes |
The idle checker monitors the notebook_locks table. As long as any non-expired lock exists, the idle timer resets. Once all locks are released (or expired), the countdown begins.
When the idle timer fires:
- A WebSocket warning is broadcast 5 minutes before shutdown
- After the timeout, all remaining locks are released
- The Marimo server process is stopped
- The PID file (
.dango/marimo.pid) is cleaned up
The next dango notebook open (or web UI Open) automatically restarts Marimo with a fresh snapshot.
Lock Storage¶
Locks are stored in the notebook_locks table in .dango/dango.db (SQLite):
| Column | Type | Description |
|---|---|---|
notebook_id | TEXT (PK) | Notebook name (filename stem) |
locked_by | TEXT | Username or email of the lock holder |
locked_at | DATETIME | When the lock was first acquired |
expires_at | DATETIME | When the lock expires (refreshed by heartbeat) |
last_heartbeat_at | DATETIME | Timestamp of the most recent heartbeat |
Troubleshooting¶
Lock appears stuck¶
If a lock persists longer than expected:
- Wait 15 minutes — the lock will expire automatically if heartbeats have stopped
- Force unlock (admin) — use the Web UI or API to force-release the lock
- Check for stale processes — if the CLI session that acquired the lock is still running but hung, kill the process and the lock will expire
"Lock not held by you" error¶
This means:
- Your lock expired while you were editing (no heartbeat for 15+ minutes)
- Another user force-released your lock
Re-open the notebook to acquire a fresh lock. Any unsaved edits in your browser may still be intact — Marimo auto-saves to the .py file.
Idle shutdown warning¶
This WebSocket notification appears when no notebooks have been actively locked for most of the idle timeout period. To prevent shutdown, open a notebook (which acquires a lock and resets the idle timer).
Related¶
- Getting Started with Notebooks — notebook setup and basics
- DuckDB Snapshots — how snapshots provide isolation
- DuckDB & Single-Writer — understanding DuckDB's write model