Skip to content

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.

For most deployments, Deckyard works great out of the box. Consider scaling when you experience:

SymptomCauseSolution
Slow permission checksRepeated database queriesEnable caching
Export timeoutsLarge presentationsBackground jobs
High database loadUncached queriesAdd caching layer
  • 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)

Redis enables distributed rate limiting, permission caching, and background job processing.

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:
Terminal window
# Option 1: Full URL
REDIS_URL=redis://localhost:6379
# Option 2: Individual settings
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-password # Optional
REDIS_DB=0 # Optional
# Disable Redis (force in-memory fallback)
REDIS_ENABLED=false
FeatureWithout RedisWith Redis
Rate limitingPer-process memoryStored in Redis, survives restarts
Permission cachePer-process memoryShared, survives restarts
Heavy exportsSynchronous (may timeout)Background processing

Prevent oversized presentations from impacting performance:

Terminal window
# Soft limits (warnings shown to users)
PRESENTATION_SOFT_SLIDE_LIMIT=100 # Default: 100 slides
PRESENTATION_SOFT_SIZE_MB=10 # Default: 10 MB
# Hard limits (operations blocked)
PRESENTATION_HARD_SLIDE_LIMIT=500 # Default: 500 slides
PRESENTATION_HARD_SIZE_MB=50 # Default: 50 MB

Control how long collaborator permissions are cached:

Terminal window
PERMISSION_CACHE_TTL_SECONDS=300 # Default: 5 minutes
PERMISSION_CACHE_MAX_SIZE=10000 # Default: 10,000 entries

Trade-offs:

  • Lower TTL = fresher data, more database queries
  • Higher TTL = better performance, slightly delayed permission changes

Control how long analytics data is stored:

Terminal window
ANALYTICS_RETENTION_DAYS=90 # Default: 90 days
ANALYTICS_IP_ANONYMIZATION_DAYS=30 # Default: 30 days

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.

Heavy operations are processed in the background when Redis is available:

OperationQueueBehavior
PDF exportexportReturns job ID, poll for result
PPTX exportexportReturns job ID, poll for result
Handoff ZIPexportReturns job ID, poll for result
Bulk backup (POST /api/bulk-export)heavyReturns 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.

  1. Request export → 202 Accepted with a job ID (export-123) and a pollUrl
  2. Poll /api/jobs/{id} for status
  3. 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.

Add ?sync=1 to a PDF, PPTX or handoff ZIP export URL to skip the queue:

/api/presentations/{id}/export/pptx?sync=1
Terminal window
curl http://localhost:4177/health
# {"status":"ok","timestamp":1706000000000}
Terminal window
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}
Terminal window
curl -H "Cookie: session=..." \
http://localhost:4177/api/jobs/export-123
# {"id":"export-123","state":"completed","progress":100,"downloadUrl":"/api/jobs/export-123/download",...}

Adjust the connection pool for your workload:

Terminal window
DATABASE_POOL_MIN=2 # Default: 2
DATABASE_POOL_MAX=10 # Default: 10

For high-traffic deployments, consider using PgBouncer as a connection pooler.

Deckyard includes optimized indexes. If you notice slow queries, check:

-- View slow queries
SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
-- Check index usage
SELECT relname, indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;

All scaling features degrade gracefully when Redis is unavailable:

FeatureWith RedisWithout Redis
Rate limitingShared, persistentPer-process, resets on restart
Permission cacheShared, persistentPer-process memory
Background jobsAsync processingSynchronous (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

”Redis unavailable, using in-memory fallback”

Section titled “”Redis unavailable, using in-memory fallback””

This warning means Redis connection failed. Check:

  1. REDIS_URL or REDIS_HOST is correct
  2. Redis is running and accessible
  3. Firewall allows the connection

The system continues working with in-memory fallback.

With caching enabled, permission changes may take up to 5 minutes. To reduce:

Terminal window
PERMISSION_CACHE_TTL_SECONDS=60 # 1 minute

For large presentations:

  1. Add Redis for background processing
  2. Use ?sync=1 if you need immediate results (may still timeout)
  3. Consider reducing presentation size

When Redis is working, the log shows:

[2026-01-01T12:00:00.000Z] [INFO] [redis] Connected successfully

If you see fallback messages, Redis isn’t working.