Describe the bug
@hguerrero reported and issue when running a Workshop:
│ [Tue Jun 16 13:52:22.127384 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/models/base.py", line 877, in save_base │
│ [Tue Jun 16 13:52:22.127387 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] updated = self._save_table( │
│ [Tue Jun 16 13:52:22.127390 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] raw, │
│ [Tue Jun 16 13:52:22.127427 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ...<4 lines>... │
│ [Tue Jun 16 13:52:22.127430 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] update_fields, │
│ [Tue Jun 16 13:52:22.127432 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ) │
│ [Tue Jun 16 13:52:22.127435 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/models/base.py", line 1020, in _save_table │
│ [Tue Jun 16 13:52:22.127438 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] results = self._do_insert( │
│ [Tue Jun 16 13:52:22.127441 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] cls._base_manager, using, fields, returning_fields, raw │
│ [Tue Jun 16 13:52:22.127445 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ) │
│ [Tue Jun 16 13:52:22.127448 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/models/base.py", line 1061, in _do_insert │
│ [Tue Jun 16 13:52:22.127451 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] return manager._insert( │
│ [Tue Jun 16 13:52:22.127454 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ~~~~~~~~~~~~~~~^ │
│ [Tue Jun 16 13:52:22.127457 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] [self], │
│ [Tue Jun 16 13:52:22.127460 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^^^^^^^ │
│ [Tue Jun 16 13:52:22.127463 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ...<3 lines>... │
│ [Tue Jun 16 13:52:22.127466 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] raw=raw, │
│ [Tue Jun 16 13:52:22.127469 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^^^^^^^^ │
│ [Tue Jun 16 13:52:22.127472 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ) │
│ [Tue Jun 16 13:52:22.127474 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^ │
│ [Tue Jun 16 13:52:22.127477 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/models/manager.py", line 87, in manager_method │
│ [Tue Jun 16 13:52:22.127480 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] return getattr(self.get_queryset(), name)(*args, **kwargs) │
│ [Tue Jun 16 13:52:22.127483 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ │
│ [Tue Jun 16 13:52:22.127486 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/models/query.py", line 1810, in _insert │
│ [Tue Jun 16 13:52:22.127490 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] return query.get_compiler(using=using).execute_sql(returning_fields) │
│ [Tue Jun 16 13:52:22.127493 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^ │
│ [Tue Jun 16 13:52:22.127496 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/models/sql/compiler.py", line 1833, in execute_sql │
│ [Tue Jun 16 13:52:22.127499 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] self.connection.ops.fetch_returned_insert_columns( │
│ [Tue Jun 16 13:52:22.127502 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ │
│ [Tue Jun 16 13:52:22.127505 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] cursor, │
│ [Tue Jun 16 13:52:22.127508 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^^^^^^^ │
│ [Tue Jun 16 13:52:22.127510 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] self.returning_params, │
│ [Tue Jun 16 13:52:22.127513 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^^^^^^^^^^^^^^^^^^^^^^ │
│ [Tue Jun 16 13:52:22.127516 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ) │
│ [Tue Jun 16 13:52:22.127518 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^ │
│ [Tue Jun 16 13:52:22.127521 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/backends/base/operations.py", line 213, in fetch_returned_insert_col │
│ [Tue Jun 16 13:52:22.127524 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] return cursor.fetchone() │
│ [Tue Jun 16 13:52:22.127527 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ~~~~~~~~~~~~~~~^^ │
│ [Tue Jun 16 13:52:22.127530 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/utils.py", line 97, in inner │
│ [Tue Jun 16 13:52:22.127533 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] with self: │
│ [Tue Jun 16 13:52:22.127536 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] ^^^^ │
│ [Tue Jun 16 13:52:22.127539 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/utils.py", line 91, in __exit__ │
│ [Tue Jun 16 13:52:22.127542 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] raise dj_exc_value.with_traceback(traceback) from exc_value │
│ [Tue Jun 16 13:52:22.127546 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] File "/opt/app-root/venv/lib64/python3.13/site-packages/django/db/utils.py", line 98, in inner │
│ [Tue Jun 16 13:52:22.127549 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] return func(*args, **kwargs) │
│ [Tue Jun 16 13:52:22.127552 2026] [wsgi:error] [pid 12:tid 34] [remote 10.1.0.3:49678] django.db.utils.OperationalError: database is locked
Additional information
He's running version 3.7.1 with a workshop with 40+ attendees.
When asked Claude about this specific issue, replied back with:
Diagnosis
The training portal is a Django app backed by SQLite (/opt/app-root/data/db.sqlite3), served by Apache mod_wsgi with multiple worker threads/processes. The DB file lives on a PersistentVolumeClaim (training-portal, mounted at
/opt/app-root/data).
database is locked is SQLite's response when a writer can't acquire the write lock within the busy-timeout. The settings give it 15s (settings.py:83), and it still timed out — so this is one of two things, often both:
1. Write contention burst — a workshop's worth of attendees hitting the portal at the same moment (registration, session start, event/heartbeat writes). SQLite serializes all writes to a single lock. The portal uses the default
rollback-journal mode (no WAL), so even reads can block writers. Under a burst, one slow write holds the lock past 15s and everyone else errors.
2. PVC filesystem locking — if that training-portal PVC is backed by NFS / a network filesystem, SQLite's POSIX fcntl locking is unreliable there and throws database is locked even at low concurrency. This is a very common cause
of this exact error in Educates portals. Worth checking the storage class.
Right now (live workshop)
- The error is transient and per-request. The attendee who hit it can just retry/refresh — their next request will usually succeed. No data corruption.
- If many are stuck, restart the portal pod — it clears any stale lock and re-serializes cleanly:
kubectl rollout restart deployment/training-portal -n <portal-namespace>
- Check what's backing the PVC (this tells you if it's the deeper problem):
kubectl get pvc training-portal -n <portal-namespace> -o jsonpath='{.spec.storageClassName}'
kubectl get pvc training-portal -n <portal-namespace> -o jsonpath='{.spec.volumeMode}'
- If the storage class is NFS-backed, that's your culprit — move the portal to a block-storage / local-path storage class.
Durable fix (code change)
Two cheap improvements in training-portal/src/project/settings.py. Want me to make these?
- Enable WAL + a real busy timeout so readers don't block the writer and locks back off properly:
"OPTIONS": {
"timeout": 15,
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=15000;",
"transaction_mode": "IMMEDIATE", # Django 5.1+; avoids upgrade-lock deadlocks
},
- (WAL only helps on a local/block filesystem — it does not fix NFS. If the PVC is NFS-backed, the storage class change is the actual fix.)
Want me to:
1. Make the WAL/busy-timeout settings change and open a branch for it, and/or
2. Dig into where the high-frequency writes come from (the event/ and schedule/ endpoints are already filtered from logs at httpd.conf — they may be the write-heavy path worth throttling)?
Describe the bug
@hguerrero reported and issue when running a Workshop:
Additional information
He's running version 3.7.1 with a workshop with 40+ attendees.
When asked Claude about this specific issue, replied back with: