Skip to content

Session Management

Session management is a critical component of web applications, allowing you to store and retrieve user data across multiple requests. sillo provides a robust, flexible session management system that's easy to configure yet powerful enough for complex applications.

Session management is a critical component of web applications, allowing you to store and retrieve user data across multiple requests. sillo provides a robust, flexible session management system that’s easy to configure yet powerful enough for complex applications.

Setting up sessions in your sillo application is straightforward:

from sillo import silloApp
from sillo.session.middleware import SessionMiddleware
app = silloApp()
# Add the session middleware with secret_key passed directly
app.use(
SessionMiddleware(
secret_key="your-secure-secret-key",
session_cookie_name="sillo_session",
cookie_path="/",
cookie_domain=None,
cookie_secure=True,
cookie_httponly=True,
cookie_samesite="lax",
session_expiration_time=86400 # 24 hours
)
)

With this minimal setup, sillo will use the default cookie-based session backend. Your routes can now access the session through the request object:

@app.get("/")
async def index(request, response):
# Access the session
counter = request.session.get("counter", 0)
counter += 1
request.session["counter"] = counter
return response.text(f"You've visited this page {counter} times")

sillo offers various configuration options for customizing session behavior:

Recommended Approach
from sillo import silloApp
from sillo.session import SessionConfig
from sillo.session.middleware import SessionMiddleware
from sillo.session.file import FileSessionManager
app = silloApp()
session_config = SessionConfig(
session_cookie_name="sillo_session",
cookie_path="/",
cookie_domain=None,
cookie_secure=True,
cookie_httponly=True,
cookie_samesite="lax",
session_expiration_time=86400, # 24 hours
manager=FileSessionManager,
session_file_storage_path="sessions",
session_file_name="session_"
)
app.use(SessionMiddleware(config=session_config, secret_key="secret-key"))
OptionDescriptionDefault
session_cookie_nameName of the cookie storing the session ID"session_id"
cookie_pathPath for which the cookie is valid"/"
cookie_domainDomain for which the cookie is validNone
cookie_secureWhether cookie should only be sent over HTTPSFalse
cookie_httponlyWhether cookie should be accessible via JavaScriptTrue
cookie_samesiteSameSite attribute ("lax", "strict", or "none")"lax"
expirySession lifetime in seconds86400 (24 hours)
managerSession backend classSignedSessionManager
@app.get("/session-demo")
async def session_demo(request, response):
# Get a value with default if not present
user_id = request.session.get("user_id", None)
# Set a value
request.session["last_visit"] = time.time()
# Check if a key exists
if "preferences" in request.session:
preferences = request.session["preferences"]
# Remove a key
if "temporary_data" in request.session:
del request.session["temporary_data"]
# Clear the entire session
# request.session.clear()
return response.json({
"user_id": user_id,
"session_keys": list(request.session.keys())
})

Sessions in sillo behave similar to dictionaries but with additional methods:

Method/PropertyDescription
session.get(key, default=None)Get a value, returning default if not present
session[key] = valueSet a session value
key in sessionCheck if key exists in the session
del session[key]Delete a key from the session
session.clear()Remove all keys from the session
session.keys()Get all keys in the session
session.items()Get all key-value pairs in the session
session.pop(key, default=None)Get and remove a key, returning default if not present
session.is_empty()Check if session has no data
session.modifiedWhether session has been modified

By default, sessions expire after 24 hours (86400 seconds). You can customize this:

# Set global session expiration time using recommended approach
session_config = SessionConfig(
session_expiration_time=3600 # 1 hour
)
app.use(SessionMiddleware(config=session_config))
# Or set per-session expiration time
@app.post("/login")
async def login(request, response):
# Authenticate user...
request.session["user_id"] = user.id
# Set this specific session to expire in 30 minutes
request.session.set_expiry(1800)
return response.json({"success": True})

sillo supports multiple session backends to store session data. Each backend has different characteristics suitable for various use cases.

The simplest session backend, storing the session data directly in a signed cookie:

# Using recommended approach for signed cookie sessions
session_config = SessionConfig(
manager=SignedSessionManager
)
app.use(SessionMiddleware(config=session_config))

Pros:

  • No server-side storage required
  • Works well in distributed environments
  • Simple setup

Cons:

  • Limited storage size (4KB cookie limit)
  • Session data sent with every request
  • Cannot be invalidated server-side

Stores session data in files on the server filesystem:

# Using recommended approach for file-based sessions
session_config = SessionConfig(
manager=FileSessionInterface,
session_file_storage_path="sessions", # Directory to store session files
session_file_name="session_" # Prefix for session files
)
app.use(SessionMiddleware(config=session_config))

Pros:

  • Unlimited session data size
  • Sessions can be invalidated server-side
  • Simple setup for single-server environments

Cons:

  • Not suitable for distributed environments
  • Requires filesystem access
  • Needs cleanup of expired session files

You can create custom session backends by implementing the BaseSessionInterface:

from sillo.session.base import BaseSessionInterface
class RedisSessionInterface(BaseSessionInterface):
"""Redis-backed session interface"""
def __init__(self, session_key=None):
super().__init__(session_key)
self.redis_client = redis.Redis()
async def load(self):
"""Load the session data from Redis"""
if not self.session_key:
return
data = self.redis_client.get(f"session:{self.session_key}")
if data:
self._data = json.loads(data)
async def save(self):
"""Save the session data to Redis"""
if not self.session_key:
self.session_key = self.generate_sid()
expiry = self.get_expiry_age()
self.redis_client.setex(
f"session:{self.session_key}",
expiry,
json.dumps(self._data)
)
self.modified = False
def get_session_key(self):
"""Return the session key"""
return self.session_key

Session management requires careful attention to security:

import secrets
# Generate a secure random key
secret_key = secrets.token_hex(32)
# For production, store this in environment variables
secret_key = os.environ.get("SECRET_KEY")
app.use(SessionMiddleware(secret_key=secret_key))
# Using recommended approach for secure cookies
session_config = SessionConfig(
cookie_secure=True, # Only send cookies over HTTPS
cookie_httponly=True, # Prevent JavaScript access
cookie_samesite="lax" # Mitigate CSRF attacks
)
app.use(SessionMiddleware(config=session_config))
# Short expiration for sensitive operations
@app.post("/banking/transfer")
async def transfer(request, response):
# Verify authentication is recent
auth_time = request.session.get("auth_time", 0)
if time.time() - auth_time > 300: # 5 minutes
return response.redirect("/re-authenticate")
# Process transfer...
@app.post("/logout")
async def logout(request, response):
# Clear session and remove cookie
request.session.clear()
return response.redirect("/login")
@app.post("/login")
async def login(request, response):
data = await request.form
username = data.get("username")
password = data.get("password")
# Authenticate user (pseudo-code)
user = authenticate_user(username, password)
if not user:
return response.redirect("/login?error=invalid_credentials")
# Store user info in session
request.session["user_id"] = user.id
request.session["username"] = user.username
request.session["auth_time"] = time.time()
request.session["is_admin"] = user.is_admin
return response.redirect("/dashboard")
@app.get("/dashboard")
async def dashboard(request, response):
# Check if user is logged in
if "user_id" not in request.session:
return response.redirect("/login")
username = request.session["username"]
return response.html(f"<h1>Welcome, {username}!</h1>")
@app.post("/logout")
async def logout(request, response):
request.session.clear()
return response.redirect("/login?message=logged_out")
@app.get("/cart")
async def view_cart(request, response):
# Initialize cart if it doesn't exist
cart = request.session.get("cart", {})
# Calculate total
total = sum(item["price"] * item["quantity"] for item in cart.values())
return response.json({
"items": cart,
"total": total
})
@app.post("/cart/add/{product_id}")
async def add_to_cart(request, response):
product_id = request.path_params.product_id
quantity = int(request.query_params.get("quantity", 1))
# Get product details (pseudo-code)
product = get_product(product_id)
if not product:
return response.json({"error": "Product not found"}, status_code=404)
# Get or initialize cart
cart = request.session.get("cart", {})
# Add or update product in cart
if product_id in cart:
cart[product_id]["quantity"] += quantity
else:
cart[product_id] = {
"name": product.name,
"price": product.price,
"quantity": quantity
}
# Save cart to session
request.session["cart"] = cart
return response.json({"success": True, "cart": cart})
@app.post("/cart/clear")
async def clear_cart(request, response):
if "cart" in request.session:
del request.session["cart"]
return response.json({"success": True})

Example 3: Multi-step Form with Session Data

Section titled “Example 3: Multi-step Form with Session Data”
@app.get("/wizard/step1")
async def wizard_step1(request, response):
# Initialize or get form data
form_data = request.session.get("wizard_data", {})
return response.html_template("wizard/step1.html", form_data=form_data)
@app.post("/wizard/step1")
async def wizard_step1_post(request, response):
form_data = await request.form
# Validate form (pseudo-code)
if not validate_step1(form_data):
return response.redirect("/wizard/step1?error=invalid_data")
# Initialize wizard data if not exists
wizard_data = request.session.get("wizard_data", {})
# Update with step 1 data
wizard_data.update({
"name": form_data.get("name"),
"email": form_data.get("email")
})
# Save back to session
request.session["wizard_data"] = wizard_data
# Proceed to next step
return response.redirect("/wizard/step2")
@app.post("/wizard/complete")
async def wizard_complete(request, response):
# Get all wizard data
wizard_data = request.session.get("wizard_data", {})
# Process the complete submission
result = process_wizard_submission(wizard_data)
# Clear wizard data from session
del request.session["wizard_data"]
return response.redirect(f"/wizard/success?id={result.id}")

A session should hold identity and a small amount of state that is genuinely per-user and per-browser: the user id, a CSRF token, a flash message, a partially completed multi-step form.

It should not hold a shopping cart of arbitrary size, a cached user object, search results, or anything you could re-derive from the database. Every one of those grows the session, and where the session lives decides what that growth costs.

Cookie-backed sessions put the data in the browser. There is no server storage and no lookup, and the limit is hard: roughly 4 KB per cookie including overhead, sent on every request to the domain. A 3 KB session is 3 KB of upload on every image request too.

Server-backed sessions store data server-side and put only an identifier in the cookie. Size is bounded by your storage, invalidation is immediate, and every request costs a lookup.

The decision is usually made for you by one requirement: if you need to revoke a session immediately — a logout that must take effect everywhere, or an admin disabling an account — you need server-side storage. A signed cookie remains valid until it expires no matter what you do.

Regenerate the session id on privilege change. Logging in must issue a new id. Without it, an attacker who can set a victim’s session cookie before login shares the session after it — session fixation, and it is still common.

Set the cookie flags. HttpOnly keeps JavaScript out, which contains the damage from an XSS. Secure keeps it off plaintext connections. SameSite=Lax blocks the cross-site POST case. All three, always.

Expire on two clocks. An idle timeout closes abandoned sessions on shared machines; an absolute lifetime bounds how long a stolen session is useful. Neither alone is sufficient.

Never trust session contents to be current. A permission cached in a session at login is a permission the user keeps after you revoke it. Store the identity in the session and look up authorization fresh.

A cookie-backed session works identically across processes because the data travels with the request. A server-backed session does not, unless the store is shared.

An in-memory session backend is per-process: with four workers, a user is logged in on one and logged out on the other three, and the symptom is “login randomly does not work”. Any deployment beyond a single process needs Redis or a database behind the session store.

The same applies to invalidation. Clearing a session in one process does nothing to the copy in another unless the store is shared.

A cookie session is convenient for a browser application: the browser manages it, it survives navigation, and revocation is immediate with a server-side store. It brings CSRF exposure with it, which is why the two guides sit next to each other.

A bearer token suits API clients and mobile apps: nothing is automatic, which removes CSRF entirely, and the client controls storage. Revocation is the hard part — a stateless token stays valid until it expires unless you keep a denylist, which puts the state back.

Most applications end up with both: sessions for the browser, tokens for the API. That is fine, provided each endpoint is clear about which it accepts. An endpoint accepting either is an endpoint whose CSRF exposure depends on how the caller authenticated.

A cookie-backed session is only as good as the key that signs it, and a leaked key means forgeable sessions. Rotating requires accepting the old key for verification while signing with the new one, for at least one session lifetime — otherwise every logged-in user is logged out at once.