Scaling
Learn when and how to scale a Deckyard instance for higher traffic and better performance. Deckyard runs as a single application instance; this page is about making that instance go further.
Do I Need to Scale?
Section titled “Do I Need to Scale?”For most deployments, Deckyard works great out of the box. Consider scaling when you experience:
| Symptom | Cause | Solution |
|---|---|---|
| Slow permission checks | Repeated database queries | Enable caching |
| Export timeouts | Large presentations | Background jobs |
| High database load | Uncached queries | Add caching layer |
Quick Decision Guide
Section titled “Quick Decision Guide”- Single server, < 50 users → No scaling needed
- Single server, large presentations → Consider Redis for async exports
- 100+ concurrent users → Redis recommended
- Multiple app instances → not supported (see One instance)
Adding Redis
Section titled “Adding Redis”Redis enables distributed rate limiting, permission caching, and background job processing.
Docker Compose
Section titled “Docker Compose”Add Redis to your docker-compose.yml:
services: redis: image: redis:7-alpine container_name: deckyard-redis restart: unless-stopped volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5
app: # ... existing config depends_on: - redis environment: REDIS_URL: redis://redis:6379
volumes: redis_data:Environment Variables
Section titled “Environment Variables”# Option 1: Full URLREDIS_URL=redis://localhost:6379
# Option 2: Individual settingsREDIS_HOST=localhostREDIS_PORT=6379REDIS_PASSWORD=your-password # OptionalREDIS_DB=0 # Optional
# Disable Redis (force in-memory fallback)REDIS_ENABLED=falseWhat Redis Enables
Section titled “What Redis Enables”| Feature | Without Redis | With Redis |
|---|---|---|
| Rate limiting | Per-process memory | Stored in Redis, survives restarts |
| Permission cache | Per-process memory | Shared, survives restarts |
| Heavy exports | Synchronous (may timeout) | Background processing |
Configuration Options
Section titled “Configuration Options”Presentation Limits
Section titled “Presentation Limits”Prevent oversized presentations from impacting performance:
# Soft limits (warnings shown to users)PRESENTATION_SOFT_SLIDE_LIMIT=100 # Default: 100 slidesPRESENTATION_SOFT_SIZE_MB=10 # Default: 10 MB
# Hard limits (operations blocked)PRESENTATION_HARD_SLIDE_LIMIT=500 # Default: 500 slidesPRESENTATION_HARD_SIZE_MB=50 # Default: 50 MBPermission Cache
Section titled “Permission Cache”Control how long collaborator permissions are cached:
PERMISSION_CACHE_TTL_SECONDS=300 # Default: 5 minutesPERMISSION_CACHE_MAX_SIZE=10000 # Default: 10,000 entriesTrade-offs:
- Lower TTL = fresher data, more database queries
- Higher TTL = better performance, slightly delayed permission changes
Analytics Retention
Section titled “Analytics Retention”Control how long analytics data is stored:
ANALYTICS_RETENTION_DAYS=90 # Default: 90 daysANALYTICS_IP_ANONYMIZATION_DAYS=30 # Default: 30 daysOne instance
Section titled “One instance”Deckyard is built to run as one application process. Redis shares rate limits, the permission cache and the job queue, but several parts still keep their state in the process that serves them, with no messaging between processes:
- live presentation sessions and the audience connections that follow them
- real-time comment and notification updates (Server-Sent Events)
- collaborative editing over WebSocket
- MCP sessions over Server-Sent Events
- uploaded media and the deck-thumbnail cache on local disk, unless you use an external media provider
Behind a load balancer with several app instances, a presenter and their audience can land on different processes and stop seeing each other. Scale the one instance up (CPU, memory, Redis, a larger database) rather than out.
Background Jobs
Section titled “Background Jobs”Heavy operations are processed in the background when Redis is available:
| Operation | Queue | Behavior |
|---|---|---|
| PDF export | export | Returns job ID, poll for result |
| PPTX export | export | Returns job ID, poll for result |
| Handoff ZIP | export | Returns job ID, poll for result |
Bulk backup (POST /api/bulk-export) | heavy | Returns job ID, poll for result |
Other exports (PNG, PPTX template, notes) always run synchronously. Without Redis, the queued operations run synchronously as well and return the file directly.
Client Flow
Section titled “Client Flow”- Request export →
202 Acceptedwith a job ID (export-123) and apollUrl - Poll
/api/jobs/{id}for status - When complete, download from
/api/jobs/{id}/download
Status and download are only available to the user who requested the export; for anyone else the job answers 404. Completed export jobs are kept for an hour.
Force Synchronous
Section titled “Force Synchronous”Add ?sync=1 to a PDF, PPTX or handoff ZIP export URL to skip the queue:
/api/presentations/{id}/export/pptx?sync=1Monitoring
Section titled “Monitoring”Health Check
Section titled “Health Check”curl http://localhost:4177/health# {"status":"ok","timestamp":1706000000000}Queue Statistics (Admin Only)
Section titled “Queue Statistics (Admin Only)”curl -H "Cookie: session=..." \ http://localhost:4177/api/jobs/queue/export/stats
# {"queueName":"export","available":true,"waiting":0,"active":1,"completed":42,"failed":0,"delayed":0,"total":1}Job Status
Section titled “Job Status”curl -H "Cookie: session=..." \ http://localhost:4177/api/jobs/export-123
# {"id":"export-123","state":"completed","progress":100,"downloadUrl":"/api/jobs/export-123/download",...}Database Tuning
Section titled “Database Tuning”Connection Pool
Section titled “Connection Pool”Adjust the connection pool for your workload:
DATABASE_POOL_MIN=2 # Default: 2DATABASE_POOL_MAX=10 # Default: 10For high-traffic deployments, consider using PgBouncer as a connection pooler.
Indexes
Section titled “Indexes”Deckyard includes optimized indexes. If you notice slow queries, check:
-- View slow queriesSELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
-- Check index usageSELECT relname, indexrelname, idx_scan, idx_tup_readFROM pg_stat_user_indexesORDER BY idx_scan ASC;Graceful Fallbacks
Section titled “Graceful Fallbacks”All scaling features degrade gracefully when Redis is unavailable:
| Feature | With Redis | Without Redis |
|---|---|---|
| Rate limiting | Shared, persistent | Per-process, resets on restart |
| Permission cache | Shared, persistent | Per-process memory |
| Background jobs | Async processing | Synchronous (may timeout) |
This means you can:
- Start without Redis and add it later
- Survive Redis outages (degraded but functional)
- Run single-instance deployments with zero additional infrastructure
Troubleshooting
Section titled “Troubleshooting””Redis unavailable, using in-memory fallback”
Section titled “”Redis unavailable, using in-memory fallback””This warning means Redis connection failed. Check:
REDIS_URLorREDIS_HOSTis correct- Redis is running and accessible
- Firewall allows the connection
The system continues working with in-memory fallback.
Permission changes not reflected
Section titled “Permission changes not reflected”With caching enabled, permission changes may take up to 5 minutes. To reduce:
PERMISSION_CACHE_TTL_SECONDS=60 # 1 minuteExports timing out
Section titled “Exports timing out”For large presentations:
- Add Redis for background processing
- Use
?sync=1if you need immediate results (may still timeout) - Consider reducing presentation size
Checking the Redis connection
Section titled “Checking the Redis connection”When Redis is working, the log shows:
[2026-01-01T12:00:00.000Z] [INFO] [redis] Connected successfullyIf you see fallback messages, Redis isn’t working.