hibernate/hibernate-orm · error · IllegalStateException

Session is in read-only mode

Error message

Session is in read-only mode

What it means

The session was opened in read-only mode (SessionBuilder.readOnly(true) / SessionCreationOptions.isReadOnly()), and checkNotReadOnly() guards every state-changing operation (persist, merge, remove, update, delete, mutating queries, flush). Any write attempt fails with IllegalStateException before the database is touched. Read-only mode is a contract, not a hint: Hibernate blocks writes rather than silently dropping or allowing them.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java:865

		);
	}

	@Nullable
	private static Object getTenantId( SessionFactoryOptions factoryOptions, SessionCreationOptions options ) {
		final Object tenantIdentifier = options.getTenantIdentifierValue();
		if ( factoryOptions.isMultiTenancyEnabled() && tenantIdentifier == null ) {
			throw new HibernateException( "SessionFactory configured for multi-tenancy, but no tenant identifier specified" );
		}
		return tenantIdentifier;
	}

	boolean isReadOnly() {
		return readOnly;
	}

	void checkNotReadOnly() {
		if ( isReadOnly() ) {
			throw new IllegalStateException( "Session is in read-only mode" );
		}
	}

	@Nonnull
	private static SessionEventListenerManager createSessionEventsManager(
			SessionFactoryOptions factoryOptions, SessionCreationOptions options) {
		final var customListeners = options.getCustomSessionEventListeners();
		return customListeners == null
				? new SessionEventListenerManagerImpl( factoryOptions.buildSessionEventListeners() )
				: new SessionEventListenerManagerImpl( customListeners );
	}

	/**
	 * Override the implementation provided on SharedSessionContractImplementor
	 * which is not very efficient: this method is hot in Hibernate Reactive, and could
	 * be hot in some ORM contexts as well.
	 */
	@Override

View on GitHub (pinned to fad1729dce)

Solutions

  1. Run writes in their own non-read-only transactional method (@Transactional(readOnly=false)); keep readOnly=true strictly for query methods.
  2. If the session came from withOptions().readOnly(true), open a normal session for the mutating work.
  3. Split large service methods so read and write concerns have separate transaction boundaries and annotations.

Example fix

// before
@Transactional(readOnly = true)
public CustomerDto load(Long id) {
    repo.saveCounter(id); // IllegalStateException: Session is in read-only mode
}
// after
@Transactional(readOnly = true)
public CustomerDto load(Long id) { return readRepo.find(id); }

@Transactional
public void recordVisit(Long id) { repo.saveCounter(id); }
Defensive patterns

Strategy: validation

Validate before calling

if (session.isDefaultReadOnly()) {
    throw new UnsupportedOperationException(
        "Session opened read-only; mutations require a writable transaction");
}
session.persist(entity);

Prevention

When it happens

Trigger: sessionFactory.withOptions().readOnly(true).openSession() (or Spring @Transactional(readOnly=true) propagating Hibernate session read-only) followed by session.persist/merge/remove, createMutationQuery(...).executeUpdate(), or session.flush().

Common situations: A @Transactional(readOnly=true) service method that 'just this once' performs a write; upgrading to Hibernate 6.4+/7 where read-only session enforcement became explicit and previously tolerated writes now throw; test fixtures opening read-only sessions for speed and then mutating data in cleanup.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/868bb7770f6119f1. Report an issue: GitHub.