Synchronized Configuration for Waitress and Gunicorn
We've improved NOW LMS to ensure both Waitress and Gunicorn WSGI servers work seamlessly out-of-the-box with identical configuration.
We've improved NOW LMS to ensure both Waitress and Gunicorn WSGI servers work seamlessly out-of-the-box with identical configuration.
The system was exhibiting erratic session behavior:
The issue was caused by cache key collision between authenticated and anonymous users.
When using @cache.cached(unless=condition):
- The unless parameter only controls whether to WRITE to cache
- It does NOT control whether to READ from cache
- All requests use the same cache key (e.g., view//home)
/homeCaches it with key view//home
Authenticated user visits /home
unless returns True (don't cache for authenticated users)Shows login button instead of user menu! ❌
Authenticated user refreshes
Use authentication-aware cache keys instead of relying on unless parameter.
def cache_key_with_auth_state() -> str:
"""Generate cache key that includes authentication state.
This ensures authenticated and anonymous users get different cached versions
of the same page.
"""
from flask import request
# Include authentication state in the cache key
auth_state = "auth" if (current_user and current_user.is_authenticated) else "anon"
# Build key from request path and auth state
key = f"view/{request.path}/{auth_state}"
# Include query parameters if present
if request.query_string:
key += f"?{request.query_string.decode('utf-8')}"
return key
| User State | Page | Cache Key |
|---|---|---|
| Anonymous | /home |
view//home/anon |
| Authenticated | /home |
view//home/auth |
| Anonymous | /course/explore?page=2 |
view//course/explore/anon?page=2 |
| Authenticated | /course/explore?page=2 |
view//course/explore/auth?page=2 |
Before:
@home.route("/home")
@cache.cached(timeout=90, unless=no_guardar_en_cache_global)
def pagina_de_inicio() -> str:
...
After:
@home.route("/home")
@cache.cached(timeout=90, key_prefix=cache_key_with_auth_state)
def pagina_de_inicio() -> str:
...
now_lms/cache.pycache_key_with_auth_state() functionKept no_guardar_en_cache_global() for backward compatibility
now_lms/vistas/home.py
Updated home page cache decorator
now_lms/vistas/courses.py
Updated 5 course view cache decorators
now_lms/vistas/resources.py
Updated 2 resource view cache decorators
now_lms/vistas/programs.py
Updated 2 program view cache decorators
tests/test_session_cache_fix.py
pytest tests/test_session_cache_fix.py -v
Tests verify: - Cache keys differ for authenticated vs anonymous users - Cache keys include query parameters - Login/logout flow works correctly - No cache collisions between user states
pytest tests/test_cache_invalidation.py tests/test_negative_simple.py -v
All existing cache tests still pass.
✅ Smooth navigation - No more flickering between authenticated/anonymous states
✅ Consistent session - Users always see the correct state for their authentication
✅ Better performance - Can still cache pages, but separately per auth state
✅ Security - Prevents authenticated content from being cached for anonymous users
The no_guardar_en_cache_global() function is kept for backward compatibility, but is no longer used in the main codebase. It can be removed in a future version after verifying no plugins or custom code depend on it.
When running NOW LMS with production WSGI servers (Gunicorn multi-process or Waitress multi-threaded), users can experience erratic session behavior:
The WSGI server does not make Flask's signed-cookie sessions worker-local. The failure occurs when NOW LMS selects server-side sessions but that interface is not actually installed, its backend is unavailable, or workers use different secrets/backends.
Gunicorn spawns multiple worker processes (e.g., gunicorn app:app --workers 4). Every process must use the same stable SECRET_KEY and the same Redis or SQLAlchemy session backend. NOW LMS also disables Gunicorn app preloading so workers do not inherit pooled database connections opened before the fork.
Waitress uses a single process with multiple threads, so it does not switch requests between process-local workers. It still uses the same server-side backend as Gunicorn, which keeps deployment behavior consistent and supports multiple NOW LMS instances behind a load balancer.
Implement shared session storage that all workers/threads can safely access:
Redis provides optimal performance and is the recommended solution for production with both Gunicorn and Waitress:
When Redis is not configured, sessions use the same SQLAlchemy database as NOW LMS:
During tests (when pytest is detected), the system uses Flask's default signed cookie sessions since tests run in a single process.
The system automatically detects the best available session storage:
# Priority order:
1. Redis (if REDIS_URL or SESSION_REDIS_URL is set)
2. SQLAlchemy database (if not in testing mode)
3. Default Flask sessions (for testing)
Set the Redis URL in your environment:
export REDIS_URL=redis://localhost:6379/0
# or
export SESSION_REDIS_URL=redis://localhost:6379/0
Then run your WSGI server of choice:
# Using Waitress (default)
lmsctl serve
# Using Gunicorn
lmsctl serve --wsgi-server gunicorn
# Or directly
gunicorn "now_lms:lms_app" --workers 4 --bind 0.0.0.0:8000
If Redis is not configured, the system automatically stores sessions in the configured SQLAlchemy database. No additional service is needed.
ALWAYS set a stable SECRET_KEY in production:
export SECRET_KEY="your-long-random-secret-key-here"
⚠️ Never use the default "dev" SECRET_KEY in production! This will cause session issues even with shared storage.
Generate a secure key:
python -c "import secrets; print(secrets.token_hex(32))"
All session storage backends use these production-ready settings:
requirements.txt: Added flask-session dependencynow_lms/session_config.py: Session configuration with cookie security settingsnow_lms/__init__.py: Integrated session initializationnow_lms/config/__init__.py: Added SECRET_KEY warningrun.py: Waitress configuration with shared session storagenow_lms/cli.py: Both Gunicorn and Waitress configuration with shared session storageRun the session configuration tests:
pytest tests/test_session_multiworker.py -v
Tests verify: - Redis configuration when REDIS_URL is set - SQLAlchemy fallback when Redis is not configured - Proper settings for production use - Flask-Login persistence across independent worker processes
NOW LMS has built-in configuration for both Waitress and Gunicorn in run.py and now_lms/cli.py with optimal settings for session handling:
# Key configurations for session support
serve(
lms_app,
host="0.0.0.0",
port=PORT,
threads=threads, # Automatically calculated based on system resources
channel_timeout=120,
cleanup_interval=30,
)
Important: - Waitress is single-process, multi-threaded - Thread count is automatically calculated based on CPU and RAM - Works well with both Redis and SQLAlchemy sessions - Cross-platform (Windows, Linux, macOS)
# Key configurations for session support
options = {
"preload_app": False, # Keep SQLAlchemy engines worker-local
"workers": workers, # Intelligent calculation based on CPU and RAM
"threads": threads, # Default 1, can be >1 for more concurrency
"worker_class": "gthread" if threads > 1 else "sync",
"graceful_timeout": 30,
}
Important:
- preload_app = False prevents workers from inheriting pooled database connections
- Works with both Redis and SQLAlchemy sessions
- Worker/thread counts are automatically calculated based on system resources
- Linux/Unix only (not supported on Windows)
The recommended way to run NOW LMS is using the built-in CLI:
# Using Waitress (default, cross-platform)
lmsctl serve
# Using Gunicorn (Linux/Unix only)
lmsctl serve --wsgi-server gunicorn
Or directly with Python:
# Uses Waitress by default
python run.py
The CLI command automatically configures your chosen WSGI server with: - Intelligent worker/thread calculation based on CPU and RAM - Environment variable support (NOW_LMS_WORKERS, NOW_LMS_THREADS) - Proper session storage configuration
export SECRET_KEY="your-secret-key"
export REDIS_URL="redis://localhost:6379/0"
export DATABASE_URL="postgresql://user:pass@localhost/dbname"
export NOW_LMS_WORKERS=4 # For Gunicorn only
export NOW_LMS_THREADS=4 # For both Waitress and Gunicorn
# Using Waitress (default)
lmsctl serve
# Using Gunicorn
lmsctl serve --wsgi-server gunicorn
waitress-serve --host=0.0.0.0 --port=8000 --threads=4 now_lms:lms_app
gunicorn "now_lms:lms_app" --workers 4 --threads 2 --bind 0.0.0.0:8000
Note: Using the CLI command (lmsctl serve) is recommended as it automatically configures the server with optimal settings.
NOW LMS automatically calculates optimal counts, but you can override:
workers = (2 × CPU cores) + 1Example for 4 CPU cores:
export NOW_LMS_WORKERS=9
lmsctl serve --wsgi-server gunicorn
Example:
export NOW_LMS_THREADS=8
lmsctl serve
gunicorn "now_lms:lms_app" --workers 4 --timeout 120 --bind 0.0.0.0:8000
waitress-serve --host=0.0.0.0 --port=8000 --threads=8 --channel-timeout=120 now_lms:lms_app
Check logs for session configuration:
INFO: Configuring Redis-based session storage for multi-worker/multi-threaded WSGI servers
INFO: Session storage initialized: redis
INFO: Using Redis for session storage - optimal for multi-worker WSGI servers
or
INFO: Configuring SQLAlchemy-based session storage for multi-worker/multi-threaded WSGI servers
INFO: Session storage initialized: sqlalchemy
INFO: Session table: flask_sessions
redis-cli ping (should return "PONG")echo $REDIS_URLecho $SECRET_KEYecho $SECRET_KEYflask_sessions table can be createdIf Redis is configured but not available:
# Disable the Redis setting to use the SQLAlchemy fallback
unset REDIS_URL
unset SESSION_REDIS_URL
Both servers work well with NOW LMS and support the same session storage configuration:
The configuration is designed to work seamlessly with both:
# No configuration changes needed!
# Using Waitress
lmsctl serve
# Using Gunicorn
lmsctl serve --wsgi-server gunicorn
Session storage configuration is shared and works identically with both servers.
| Feature | Redis | SQLAlchemy |
|---|---|---|
| Speed | Very Fast | Database-dependent |
| Scalability | Excellent | Good |
| Multi-server | Yes | Yes, with a shared database |
| Persistence | Configurable | Yes |
| Setup | Requires Redis | Uses the NOW LMS database |
| Concurrent workers | Yes | Yes |
This guide explains how NOW LMS automatically optimizes RAM usage through intelligent worker and thread configuration for production WSGI servers (Gunicorn and Waitress).
NOW LMS implements RAM-aware configuration that automatically calculates the optimal number of workers and threads based on:
This ensures the application doesn't consume more RAM than available, preventing crashes and performance issues.