Tecnología · GUÍA PRÁCTICA
¿Python dice “database is locked”? Revisa el código y reintenta la fase correcta
Un plazo mayor no actualiza una instantánea WAL obsoleta. Registra el código, la fase fallida y el modo de transacción de Python antes de elegir qué repetir.

El mismo mensaje database is locked puede exigir esperar a otro escritor, descartar una instantánea de lectura obsoleta o terminar un commit pendiente. Repetir a ciegas el último UPDATE puede conservar decisiones antiguas o aplicar un cambio dos veces.
Empieza por la conexión que lanzó la excepción. Registra las versiones de Python y de SQLite en ejecución, la identidad de la base, el modo de diario, el control de transacciones y si falló BEGIN, UPDATE o COMMIT. Reproduce el caso en una base desechable antes de cambiar la política de la aplicación.
Conserva el código extendido y la fase fallida
Desde Python 3.11, las excepciones originadas en SQLite incluyen sqlite_errorcode y sqlite_errorname. Regístralos con getattr(exc, "sqlite_errorcode", None) y getattr(exc, "sqlite_errorname", None); conserva el valor completo antes de extraer la categoría primaria con code & 255. SQLITE_BUSY vale 5, SQLITE_LOCKED 6 y SQLITE_BUSY_SNAPSHOT 517.
BUSY suele implicar otra conexión; LOCKED puede afectar a una sentencia pendiente en la misma conexión o a una caché compartida. En nuestra prueba, un cursor SELECT parcialmente consumido y aún abierto hizo que DROP TABLE devolviera database table is locked, código 6. Termina o cierra el cursor real tras su trabajo previsto; no mates procesos ajenos basándote solo en el texto del error.
Confirma quién controla los límites
Python 3.12 introdujo autocommit. Con autocommit=True, Connection.commit() y rollback() no hacen nada, incluso si abriste una transacción con SQL BEGIN. Usa SQL COMMIT o ROLLBACK explícitos para esa transacción y consulta in_transaction. Con autocommit=False, commit y rollback abren inmediatamente otra transacción DEFERRED. isolation_level solo controla LEGACY_TRANSACTION_CONTROL.
No cambies de modo como atajo para reparar una transacción activa: pasar autocommit a True confirma trabajo pendiente. Un framework también puede controlar esos límites. El gestor de contexto actual de la conexión revierte un commit fallido, pero no actúa con autocommit=True ni cierra la conexión. El defecto de CPython de 2016–2021 ya cerrado es evidencia histórica, no prueba de que siga presente.
Elige qué repetir antes de ampliar la espera
Si BEGIN IMMEDIATE falla con BUSY antes de empezar la operación, puedes reintentar adquirir el turno de escritura dentro de un presupuesto limitado. Mantén cortas las transacciones y evita llamadas de red mientras las retienes. Un BEGIN IMMEDIATE exitoso no garantiza universalmente que todas las operaciones posteriores eviten BUSY en cualquier modo de diario.
Si una transacción de lectura WAL devuelve 517 después de que otra conexión confirme cambios, termina esa transacción, abre otra, vuelve a leer y recalcula la decisión. Esperar o repetir solo UPDATE no actualiza la instantánea antigua. En modo de diario de reversión, un COMMIT bloqueado por otro lector puede devolver BUSY y mantener activa la transacción: cuando desaparezca esa condición documentada, repite COMMIT, no las escrituras ya aplicadas.
Qué mostró la prueba desechable con dos conexiones
Ejecutamos cuatro casos pequeños con Python 3.12.14, SQLite 3.53.1 en ejecución, timeout=0.05 segundos, archivos temporales y límites SQL explícitos. No usamos ninguna base del usuario. En WAL, A leyó el contador 10; B lo cambió a 11; la escritura de A devolvió 517. Con autocommit=True, A.rollback() dejó in_transaction=True. SQL ROLLBACK lo desactivó; un nuevo BEGIN IMMEDIATE leyó 11 y un único incremento confirmó 12.
Mientras la primera conexión tenía el turno de escritura, el BEGIN IMMEDIATE de la segunda devolvió código 5 y no dejó activa una transacción en esa conexión. Por separado, con diario DELETE, una lectura abierta bloqueó el COMMIT de la otra conexión: código 5, transacción aún activa. Tras terminar la lectura, repetir solo COMMIT llevó el valor inicial 10 a 11. Son casos sintéticos reproducibles, no un diagnóstico de una aplicación desconocida ni una prueba de seguridad en producción.
Termina con límites y una recuperación observable
El timeout de una conexión es un presupuesto de espera, no una solución para una instantánea obsoleta o un interbloqueo. SQLite puede omitir el busy handler si esperar no resuelve el ciclo. Registra intentos, tiempo transcurrido, código, fase y estado; limita los reintentos y comunica el fallo al agotarlos. No trates todos los OperationalError como contención reintentable.
Antes de aceptar la solución, comprueba el valor persistente previsto desde otra conexión, confirma que terminó la transacción correspondiente y repite el caso de contención. Con autocommit=False, una nueva transacción vacía puede mantener in_transaction=True: interprétalo según el modo. No borres archivos WAL o de memoria compartida para “desbloquear” la base. Si hay corrupción, errores de E/S o límites de transacción inciertos, detén este procedimiento e investiga esa causa distinta.
