oauth2-proxy/oauth2-proxy · warning

timeout obtaining session lock

Error message

timeout obtaining session lock

What it means

refreshSessionIfNeeded in pkg/middleware/stored_session.go obtains a session lock (to safely refresh a session near expiry) with a bounded wait of sessionRefreshObtainTimeout. If the lock is not obtained before the context deadline, the loop's ctx.Done() branch returns this error. It prevents concurrent requests from deadlocking on the same session.

Source

Thrown at pkg/middleware/stored_session.go:168

}

// refreshSessionIfNeeded will attempt to refresh a session if the session
// is older than the refresh period.
// Success or fail, we will then validate the session.
func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) error {
	if !needsRefresh(s.refreshPeriod, session) {
		// Refresh is disabled or the session is not old enough, do nothing
		return nil
	}

	var lockObtained bool
	ctx, cancel := context.WithTimeout(context.Background(), sessionRefreshObtainTimeout)
	defer cancel()

	for !lockObtained {
		select {
		case <-ctx.Done():
			return errors.New("timeout obtaining session lock")
		default:
			err := session.ObtainLock(req.Context(), sessionRefreshLockDuration)
			if err != nil && !errors.Is(err, sessionsapi.ErrLockNotObtained) {
				return fmt.Errorf("error occurred while trying to obtain lock: %v", err)
			} else if errors.Is(err, sessionsapi.ErrLockNotObtained) {
				time.Sleep(sessionRefreshRetryPeriod)
				continue
			}
			// No error means we obtained the lock
			lockObtained = true
		}
	}

	// The rest of this function is carried out under lock, but we must release it
	// wherever we exit from this function.
	defer func() {
		if session == nil {
			return

View on GitHub (pinned to 33c2eb92de)

Solutions

  1. Tune sessionRefreshObtainTimeout/sessionRefreshRetryPeriod to tolerate expected lock contention
  2. Check the session store (e.g. Redis) for stale locks left by crashed requests and clear them
  3. Retry the request after the lock holder completes; the error is transient by design
  4. Investigate duplicate concurrent refreshes (polling clients) that create lock storms
Defensive patterns

Strategy: retry

Validate before calling

// before issuing a request that will trigger refresh, check remaining lock headroom
if sessionRefreshObtainTimeout < sessionRefreshRetryPeriod {
	return errors.New("obtain timeout shorter than retry period")
}

Try / catch

session, err := getValidatedSession(req)
if err != nil {
	if strings.Contains(err.Error(), "timeout obtaining session lock") {
		// transient: back off and retry once
		time.Sleep(time.Second)
		return getValidatedSession(req)
	}
	return err
}

Prevention

When it happens

Trigger: Another request holds the session lock (session.ObtainLock keeps returning ErrLockNotObtained) for longer than sessionRefreshObtainTimeout while this request tries to refresh a session near its expiry.

Common situations: Long-running upstream request holding the lock while a parallel tab/polling request needs refresh; stuck lock in the session store after a crashed request; very short sessionRefreshObtainTimeout under load.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of oauth2-proxy/oauth2-proxy@33c2eb92de (2026-09-06). Data as JSON: /api/errors/7d198d583cb9f413. Report an issue: GitHub.