Quick answer
Run SELECT * FROM MON$STATEMENTS and SELECT * FROM MON$TRANSACTIONS to find the transaction holding the lock, then kill it via gfix -k or commit/rollback from the app. If that's not possible, bump LockTimeout in firebird.conf to 15–20 seconds.
Why this happens
Firebird uses a lock manager to keep multiple transactions from stepping on each other. When transaction A updates a row or table, it holds a lock until commit or rollback. Transaction B comes along, tries to update the same row, and waits. If B doesn't get the lock within the time set by LockTimeout (default is 10 seconds), it throws FWP_E_TIMEOUT with the error code 0X80320012.
Most of the time, this isn't a random glitch. It's a pattern — someone left a transaction open in a dev tool, a report job runs long and holds a lock, or an app has a bug where it starts a transaction and never commits. Missing indexes also cause it because Firebird has to lock more rows during updates and deletes, making contention worse.
I've seen this exact error on Firebird 2.5 and 3.0 in production during end-of-day batch jobs. The culprit is almost always a forgotten COMMIT in a loop or a cursor that's still open.
Fix steps
1. Find the blocking transaction
If you're on Firebird 2.1 or newer, you have the MON$ tables. Open your SQL tool (ISQL, FlameRobin, or whatever you use) and run these two queries:
SELECT MON$STATEMENT_ID, MON$SQL_TEXT, MON$TRANSACTION_ID
FROM MON$STATEMENTS
WHERE MON$STATE = 1; -- active statements
SELECT MON$TRANSACTION_ID, MON$ATTACHMENT_ID, MON$STATE, MON$TIMESTAMP
FROM MON$TRANSACTIONS
WHERE MON$STATE = 1; -- active transactionsLook for a statement that's been running for a long time or a transaction that's been idle but still open. Those are your usual suspects. The MON$ATTACHMENT_ID is what you need to kill the connection.
2. Kill the blocking connection
Once you've got the attachment ID, you can disconnect it. If you're on Windows or Unix, run this from the command line:
gfix -k -attach <attachment_id> -user sysdba -password <password> <database_path>This forces the attachment to roll back its active transaction and releases the locks. It's blunt, but it works. You'll see the error go away immediately.
If you don't have gfix handy, you can also kill the process from the OS side (Task Manager or kill on Unix). But that's riskier — it might leave the database in a weird state, though Firebird recovers on next connect.
3. If you can't kill the blocker, raise LockTimeout
Sometimes you can't kill the other transaction because it's a legit long-running process. For those cases, edit firebird.conf and find this line:
#LockTimeout = 10Change it to something reasonable, like 20 or 30 seconds. The default 10 seconds is too aggressive for heavy batch operations. After you change it, restart Firebird.
LockTimeout = 20Don't go crazy with 60+ seconds — users will hang and you'll get complaints about 'frozen' screens. 20–30 is the sweet spot.
If that doesn't fix it
If you're still getting timeouts after killing the blocker and bumping the timeout, the problem might be a missing index. When Firebird updates a table, it has to lock the rows that match the WHERE clause. Without an index, it does a full table scan and locks more rows than necessary, increasing the chance of collision.
Check your slow queries and the execution plans. If you see NATURAL scans in the plan for updates or deletes, create an index on the columns used in the WHERE clause. Example:
CREATE INDEX IDX_ORDERS_STATUS ON ORDERS (STATUS, ORDER_DATE);Another possibility is that your application is using pessimistic locking unnecessarily. If you're doing SELECT ... FOR UPDATE and then updating, consider switching to optimistic concurrency (just update and check ROW_COUNT). That reduces lock holding time.
Also check your transaction isolation level. If you're using SNAPSHOT (read committed) in Firebird 3, that's fine. But if you're on CONSISTENCY, that takes table-level locks and causes more contention. Switch to read committed if your app allows it.
Prevention tips
The real fix here is to stop the pattern that causes it. Set a strict rule in your app: every transaction must be wrapped in try-catch-finally, and COMMIT or ROLLBACK must always happen.
Also run a periodic sweep for idle transactions. In Firebird, you can set IdleTimeout in firebird.conf to kill connections that sit idle too long:
IdleTimeout = 120That's in seconds, so 120 means 2 minutes. If you've got apps that hold connections open and forget to commit, this will save you.
Last but not least, monitor your MON$ tables during peak hours. You'll see the problem coming before users experience it.