gofiber/fiber · error

limiter: failed to persist state: %w

Error message

limiter: failed to persist state: %w

What it means

Returned by SlidingWindow.New's handler (limiter_sliding.go:91-94) when manager.set fails to persist the sliding-window counters (currHits, prevHits, exp) after incrementing currHits on the request path. Storage write or marshal failure.

Source

Thrown at middleware/limiter/limiter_sliding.go:93

		// Calculate how many hits can be made based on the current rate
		remaining := maxRequests - rate

		// Update storage. Garbage collect when the next window ends.
		// |--------------------------|--------------------------|
		//               ^            ^               ^          ^
		//              ts         e.exp   End sample window   End next window
		//               <------------>
		// 				   Reset In Sec
		// resetInSec = e.exp - ts - time until end of current window.
		// duration + expiration = end of next window.
		// Because we don't want to garbage collect in the middle of a window
		// we add the expiration to the duration.
		// Otherwise, after the end of "sample window", attackers could launch
		// a new request with the full window length.
		if setErr := manager.set(reqCtx, key, e, ttlDuration(resetInSec, expiration)); setErr != nil {
			mux.Unlock()
			return fmt.Errorf("limiter: failed to persist state: %w", setErr)
		}

		// Unlock entry
		mux.Unlock()

		// Check if hits exceed the allowed maximum for this request
		if remaining < 0 {
			// Return response with Retry-After header
			// https://tools.ietf.org/html/rfc6584
			if !cfg.DisableHeaders {
				c.Set(fiber.HeaderRetryAfter, utils.FormatUint(resetInSec))
			}

			// Call LimitReached handler
			return cfg.LimitReached(c)
		}

		// Continue stack for reaching c.Response().StatusCode()

View on GitHub (pinned to 9a4c7e57fe)

Solutions

  1. Verify Storage health and that it accepts the TTL range produced by ttlDuration (which can be very large on overflow).
  2. Run `make generate` so item MarshalMsg matches the struct.
  3. If TTL overflow is plausible (very large ExpirationFunc), clamp the value upstream.
  4. Wrap the limiter in a fail-open handler if a storage blip should not 500 the client.

Example fix

// before
app.Use(limiter.New(limiter.Config{Storage: redis.New()}))

// after — bounded expiration + fail-open
store := redis.New()
app.Use(func(c fiber.Ctx) error {
    if err := sliding.New(&limiter.Config{
        Storage: store,
        ExpirationFunc: func(c fiber.Ctx) time.Duration { return time.Minute },
    })(c); err != nil {
        log.Printf("limiter persist failed: %v", err)
        return c.Next()
    }
    return nil
})
Defensive patterns

Strategy: fallback

Validate before calling

// sanity check that ttlDuration outputs are accepted by your storage
// e.g. Redis accepts up to ~292 years; very large but valid

Try / catch

// fail open around the limiter handler so storage blips do not 500 the client

Prevention

When it happens

Trigger: Configured remote Storage errors on SetWithContext, or the item MarshalMsg fails (codegen drift). The TTL passed is ttlDuration(resetInSec, expiration) which can be math.MaxInt64 on overflow — a storage that rejects very large TTLs could fail here.

Common situations: Redis unreachable mid-request; storage rejects the computed TTL; msgp-generated code out of sync; storage pool saturation under bursty traffic.

Related errors


AI-assisted analysis of gofiber/fiber@9a4c7e57fe (2026-08-04). Data as JSON: /data/errors/76e6c79b0e56d90a.json. Report an issue: GitHub.