Summary
middleware/session persists a fresh, never-modified session on every request that arrives without a session cookie. The post-handler guard is:
} else if sess.destroyed {
// clear cookie
} else if sess.modified || sess.fresh { // <-- sess.fresh alone triggers a store write
...
saveErr := kv.Set(reqCtx, sess.id, buf, expiry)
...
}
A cookieless request takes the !loaded branch, which sets sess.fresh = true and a new random id. If the handler never touches the session (sess.modified stays false), the middleware still writes a brand-new row to the store and sets a cookie.
Impact
Any endpoint fronted by the session middleware creates one store row per anonymous request. Under load from clients that don't carry the cookie (health checks, crawlers, API clients, a benchmark/validation walker) the session store grows without bound.
Observed in probatorium's driver_postgres validation refapp: a tier-1 walker (30 concurrent, cookieless, ~3,500 req/s) grew celeris_sessions by ~5 MB/s (≈one 1.5 KB row per request), reaching 1.1 GB in 200 s and filling the fixture's tmpfs — while the users table it was actually exercising stayed at 8 MB. The session store's 5-minute expiry cleanup does not keep up (rows are created far faster than they expire, and within a burst none have expired yet).
Expected
Persist a fresh session only if the handler actually wrote to it (sess.modified), i.e. lazy session creation — the common behaviour for session libraries. A fresh-but-empty session should not hit the store or set a cookie. If eager creation is ever desired it should be opt-in (e.g. a Config.SaveUnmodified / AlwaysCreate flag), not the default.
Repro
Install session.New(session.Config{Store: <any store>}) globally; send N requests with no session cookie to a handler that never calls c.Session(). The store ends up with N rows.
Workaround
Don't install the session middleware on routes that don't use it (the probatorium refapp was fixed this way).
Summary
middleware/sessionpersists a fresh, never-modified session on every request that arrives without a session cookie. The post-handler guard is:A cookieless request takes the
!loadedbranch, which setssess.fresh = trueand a new random id. If the handler never touches the session (sess.modifiedstays false), the middleware still writes a brand-new row to the store and sets a cookie.Impact
Any endpoint fronted by the session middleware creates one store row per anonymous request. Under load from clients that don't carry the cookie (health checks, crawlers, API clients, a benchmark/validation walker) the session store grows without bound.
Observed in probatorium's
driver_postgresvalidation refapp: a tier-1 walker (30 concurrent, cookieless, ~3,500 req/s) grewceleris_sessionsby ~5 MB/s (≈one 1.5 KB row per request), reaching 1.1 GB in 200 s and filling the fixture's tmpfs — while theuserstable it was actually exercising stayed at 8 MB. The session store's 5-minute expiry cleanup does not keep up (rows are created far faster than they expire, and within a burst none have expired yet).Expected
Persist a fresh session only if the handler actually wrote to it (
sess.modified), i.e. lazy session creation — the common behaviour for session libraries. A fresh-but-empty session should not hit the store or set a cookie. If eager creation is ever desired it should be opt-in (e.g. aConfig.SaveUnmodified/AlwaysCreateflag), not the default.Repro
Install
session.New(session.Config{Store: <any store>})globally; send N requests with no session cookie to a handler that never callsc.Session(). The store ends up with N rows.Workaround
Don't install the session middleware on routes that don't use it (the probatorium refapp was fixed this way).