When PostgreSQL raises ERROR: invalid locator specification with SQLSTATE 0F001 and condition name invalid_locator_specification, it means that a locator value — an OID or descriptor used to reference a large object (LOB) or a cursor — is either syntactically invalid or refers to a resource that does not exist within the current session or transaction.
What This Error Means
SQLSTATE class 0F is defined in the SQL standard as the "locator exception" class. The single child code 0F001 (invalid_locator_specification) covers all cases where a locator value cannot be resolved to a valid, accessible resource. In PostgreSQL, this surfaces primarily in the context of large objects (stored in pg_largeobject) and their associated file descriptors opened with lo_open().
A large object locator in PostgreSQL is an OID — a 32-bit unsigned integer that identifies the object in pg_largeobject_metadata. When you call lo_open(oid, mode), PostgreSQL returns an integer file descriptor (fd) that is valid only for the lifetime of the current transaction. Passing an fd that was never opened, has already been closed, or was opened in a different transaction to functions like lo_read(), lo_write(), lo_lseek(), or lo_close() results in this error.
The error aborts the current statement but does not necessarily terminate the connection or the enclosing transaction. However, because large object operations are transactional in PostgreSQL, any uncommitted large object modifications in the same transaction will be rolled back if you subsequently issue a ROLLBACK.
Common Causes
Using a large object file descriptor outside the transaction it was opened in.
lo_open()returns a per-transaction fd. Storing that integer and reusing it in a later transaction — or after the originating transaction committed or rolled back — produces0F001.Passing a raw OID to large object functions that expect an fd.
lo_read(fd, len)requires the integer returned bylo_open(), not the OID of the large object itself. Confusing the two is a common mistake, especially when working with the low-level SQL API.Calling large object functions after
lo_close(). Once an fd is closed withlo_close(fd), that descriptor is invalid. Any subsequent call referencing the same fd raises this error.Large object deleted mid-transaction. If another session (or an earlier statement in the same transaction) deletes the underlying large object via
lo_unlink(oid)after you have opened it, some operations may fail with a locator error depending on the access pattern.Application-level bugs in connection-pooling setups. Connection poolers that reset session state between uses may leave an application holding a stale fd integer that has no meaning in the new backend session.
How to Fix invalid_locator_specification
Open the large object fd within the same transaction where you use it. Always follow the pattern:
BEGIN→lo_open()→ operations →lo_close()→COMMIT/ROLLBACK. Never cache the returned fd across transaction boundaries.BEGIN; SELECT lo_create(0); -- returns new OID, e.g. 16432 -- store the OID (not the fd) for future reference DO $$ DECLARE loid oid := 16432; fd int; BEGIN fd := lo_open(loid, 131072); -- INV_WRITE = 131072 PERFORM lo_write(fd, 'hello world'); PERFORM lo_close(fd); END; $$; COMMIT;Use the OID for persistence, the fd only for I/O within a transaction. Store the OID in your application or in a table column of type
oidorlo. Reopen the large object each time you need to read or write it.-- Correct: re-open each time BEGIN; SELECT lo_get(16432); -- shorthand for open/read/close in one call (PostgreSQL 9.4+) COMMIT;Prefer
lo_get()/lo_put()for simple access patterns. Available since PostgreSQL 9.4, these functions open, operate on, and close the large object internally, avoiding the fd lifecycle entirely.-- Read entire large object as bytea SELECT lo_get(16432); -- Write/replace content SELECT lo_put(16432, 0, '\xDEADBEEF'::bytea);Check that the OID exists before opening. If your application cannot guarantee the large object exists, verify with a catalog query before calling
lo_open().SELECT EXISTS ( SELECT 1 FROM pg_largeobject_metadata WHERE oid = 16432 );Audit connection pool reset behavior. If you use PgBouncer or a similar pooler in transaction mode, ensure your application never tries to reuse a large object fd after a transaction boundary, because each
BEGINmay arrive at a different backend.
Additional Information
- SQLSTATE class
0Fand code0F001are defined by the SQL standard; PostgreSQL's mapping is faithful to the standard definition. - Related SQLSTATE codes in neighboring classes:
22004(null_value_not_allowed) and22023(invalid_parameter_value) can also appear in large object workflows when wrong argument types are passed. lo_get()andlo_put()(PostgreSQL 9.4+) are the preferred modern API for most use cases and eliminate the fd-lifecycle class of bugs entirely.- The libpq large object API (
lo_open,lo_read,lo_write, etc.) maps directly to the SQL-level functions; the same fd-scope rules apply when using the C API or any driver that wraps it (e.g., psycopg2'slobject, JDBC'sLargeObject). - In psycopg2, accessing a
lobjectoutside its originating transaction raisespsycopg2.errors.InvalidLocatorSpecification(mapped from SQLSTATE0F001). - Large objects are transactional: modifications are rolled back on transaction abort, and newly created OIDs are invisible until the creating transaction commits.
Frequently Asked Questions
Why does my large object fd work in psql but fail in my application?
In psql, each command runs in its own implicit transaction unless you issue an explicit BEGIN. Your application likely opens the fd in one transaction and tries to use it in another (or outside any transaction). Ensure your application wraps all lo_open() / lo_read() / lo_close() calls within a single explicit transaction.
Can I pass a large object OID directly to lo_read()?
No. lo_read(fd, len) expects the integer file descriptor returned by lo_open(), not the OID. If you pass an OID that happens to be a small integer, PostgreSQL will not find a matching open fd and will raise 0F001. Use lo_open(oid, mode) first to get a valid fd.
Is SQLSTATE 0F001 ever raised outside of large object operations?
In standard SQL, 0F001 can theoretically apply to any locator type (including cursors), but PostgreSQL does not use cursor locators in the SQL-standard sense. In practice, you will only see this error from the large object API (lo_open, lo_read, lo_write, lo_lseek, lo_close, lo_tell, lo_truncate).
Does this error leave my transaction in an aborted state?
The error aborts the current statement. Whether the whole transaction is aborted depends on your error-handling setup. In PostgreSQL, any error within a transaction block (BEGIN...COMMIT) puts the transaction into an error state that requires ROLLBACK (or ROLLBACK TO SAVEPOINT) before further work can proceed. Use savepoints around large object operations if you need to recover without losing the entire transaction.