Scaling Guide

This guide covers strategies for scaling Armature applications to handle increased load.

Table of Contents

Overview

Armature applications can scale to handle millions of requests by:

  1. Horizontal Scaling - Adding more instances
  2. Vertical Scaling - Increasing instance resources
  3. Load Balancing - Distributing traffic evenly
  4. Caching - Reducing database load
  5. Async Processing - Offloading heavy operations

Horizontal Scaling

Stateless Design

Armature encourages stateless architecture for easy horizontal scaling:

// ❌ Bad: Server-side session storage
struct UserController {
    sessions: HashMap<String, Session>, // Can't scale!
}

// ✅ Good: JWT-based stateless auth
struct UserController {
    jwt_manager: JwtManager, // Stateless, can scale!
}

Using Ferron for Load Balancing

use armature_ferron::{FerronConfig, LoadBalancer, LoadBalanceStrategy, Backend};

let config = FerronConfig::builder()
    .domain("api.example.com")
    .load_balancer(
        LoadBalancer::new()
            .strategy(LoadBalanceStrategy::RoundRobin)
            .backend(Backend::new("http://app1:3000"))
            .backend(Backend::new("http://app2:3000"))
            .backend(Backend::new("http://app3:3000"))
            .backend(Backend::new("http://app4:3000"))
            .health_check_interval(10)
            .health_check_path("/health")
    )
    .build()?;

Service Discovery

Dynamic backend registration for auto-scaling:

use armature_ferron::ServiceRegistry;

let registry = ServiceRegistry::new();

// Register new instances as they come online
let id = registry.register("api-service", "http://new-instance:3000").await?;

// Deregister when scaling down
registry.deregister("api-service", &id).await?;

// Get current backends for load balancer
let backends = registry.get_healthy_urls("api-service").await;

Kubernetes HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: armature-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: armature-api
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Vertical Scaling

Rust Performance Advantages

Armature benefits from Rust's efficiency:

  • Low memory footprint - No garbage collector overhead
  • High CPU utilization - Zero-cost abstractions
  • Predictable latency - No GC pauses

Optimal Resource Allocation

# Kubernetes resource limits
resources:
  limits:
    cpu: "4"
    memory: "2Gi"
  requests:
    cpu: "1"
    memory: "512Mi"

Tokio Runtime Tuning

#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
    // Configure based on CPU cores
}

Load Balancing

Load Balancing Strategies

use armature_ferron::LoadBalanceStrategy;

// Round Robin - Equal distribution
LoadBalanceStrategy::RoundRobin

// Least Connections - Route to least busy
LoadBalanceStrategy::LeastConnections

// IP Hash - Sticky sessions without state
LoadBalanceStrategy::IpHash

// Weighted - Based on server capacity
LoadBalanceStrategy::Weighted

Weighted Load Balancing

let lb = LoadBalancer::new()
    .strategy(LoadBalanceStrategy::Weighted)
    .backend(Backend::new("http://big-server:3000").weight(10))
    .backend(Backend::new("http://small-server:3000").weight(5));

Database Scaling

Connection Pooling

use sqlx::postgres::PgPoolOptions;

let pool = PgPoolOptions::new()
    .max_connections(100)  // Max connections per instance
    .min_connections(10)   // Keep 10 warm
    .acquire_timeout(Duration::from_secs(5))
    .idle_timeout(Duration::from_secs(600))
    .connect(&database_url)
    .await?;

Read Replicas

// Write to primary
let primary = PgPool::connect("postgres://primary:5432/db").await?;

// Read from replicas
let replicas = vec![
    PgPool::connect("postgres://replica1:5432/db").await?,
    PgPool::connect("postgres://replica2:5432/db").await?,
];

// Route queries appropriately
async fn get_user(id: u32, replicas: &[PgPool]) -> Result<User, Error> {
    let replica = &replicas[id as usize % replicas.len()];
    sqlx::query_as("SELECT * FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(replica)
        .await
}

Caching Strategies

Multi-Tier Caching

use armature_cache::{TieredCache, InMemoryCache, RedisCache};

let cache = TieredCache::new()
    .l1(InMemoryCache::new(1000)) // Fast, local
    .l2(RedisCache::new(redis_url)) // Shared, persistent
    .build();

// Automatic fallthrough
let user = cache.get_or_set("user:123", || {
    fetch_user_from_db(123)
}).await?;

Cache Invalidation

use armature_cache::TaggedCache;

let cache = TaggedCache::new(redis_url);

// Set with tags
cache.set_with_tags(
    "user:123",
    user,
    &["users", "user:123"],
    Duration::from_secs(3600)
).await?;

// Invalidate by tag
cache.invalidate_tag("users").await?; // Clears all user caches

Async Processing

Job Queues

Offload heavy operations to background workers:

use armature_queue::{Queue, Job};

// Enqueue job instead of processing synchronously
queue.enqueue("send_email", EmailJob {
    to: user.email,
    subject: "Welcome!",
    template: "welcome",
}).await?;

// Return immediately to client
HttpResponse::accepted()

Worker Scaling

# Scale workers independently
apiVersion: apps/v1
kind: Deployment
metadata:
  name: email-worker
spec:
  replicas: 5  # Scale workers based on queue depth

Performance Optimization

Response Compression

use armature_framework::compression::CompressionConfig;

let config = CompressionConfig::default()
    .gzip(true)
    .brotli(true)
    .min_size(1024); // Only compress > 1KB

Connection Keep-Alive

// Ferron handles this automatically
let config = FerronConfig::builder()
    .keepalive(true)
    .keepalive_timeout(60)
    .build()?;

Request Batching

// GraphQL DataLoader pattern
#[routes]
impl UserController {
    #[post("/users/batch")]
    async fn batch_get(req: HttpRequest) -> Result<HttpResponse, Error> {
        let ids: Vec<u32> = req.json()?;

        // Single query for all users
        let users = sqlx::query_as("SELECT * FROM users WHERE id = ANY($1)")
            .bind(&ids)
            .fetch_all(&pool)
            .await?;

        HttpResponse::json(&users)
    }
}

Monitoring for Scale

Key Metrics

use armature_framework::metrics::{counter, histogram};

// Track request rate
counter!("http_requests_total", "endpoint" => path);

// Track latency
histogram!("http_request_duration_seconds", duration);

// Track errors
counter!("http_errors_total", "status" => status.to_string());

Alerting Thresholds

# Prometheus alerting rules
groups:
  - name: scaling
    rules:
      - alert: HighRequestRate
        expr: rate(http_requests_total[5m]) > 10000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Consider scaling up"

      - alert: HighLatency
        expr: histogram_quantile(0.95, http_request_duration_seconds) > 1
        for: 5m
        labels:
          severity: critical

Load Testing

# Using oha (Ohayou HTTP load generator)
oha -n 100000 -c 100 http://localhost:3000/api/users

# Using wrk
wrk -t12 -c400 -d30s http://localhost:3000/api/users

Summary

Scaling checklist:

  1. Design stateless - Enable horizontal scaling
  2. Use Ferron - High-performance load balancing
  3. Enable service discovery - Dynamic backend management
  4. Configure HPA - Automatic pod scaling
  5. Pool connections - Efficient database usage
  6. Implement caching - Reduce database load
  7. Use job queues - Offload heavy operations
  8. Monitor metrics - Data-driven scaling decisions