Disciplines · Integrations

ComfyUI Performance Tuning Guide

Configure rate limiting to avoid hitting provider limits:

11sections3 minread

On this page

This guide covers performance optimization strategies for ComfyUI workflows in the Oshun platform, including rate limiting, caching, scaling, and cost optimization.

Table of Contents#


Performance Overview#

Key Performance Factors#

text
┌─────────────────────────────────────────────────────────────────────────────┐
│                    ComfyUI Performance Stack                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Network Layer                                                               │
│  ├── API Latency: 50-200ms (request/response overhead)                      │
│  ├── Upload Time: Variable (input images, masks)                            │
│  └── Download Time: Variable (generated outputs)                            │
│                                                                              │
│  Queue Layer                                                                 │
│  ├── Queue Wait: 0-60s (depending on provider load)                         │
│  ├── Cold Start: 15-120s (if no warm workers)                               │
│  └── Worker Assignment: 1-5s                                                │
│                                                                              │
│  Execution Layer                                                             │
│  ├── Model Loading: 5-30s (cached: <1s)                                     │
│  ├── Sampling: 10-120s (varies by steps, resolution)                        │
│  ├── VAE Decode: 1-5s                                                       │
│  └── Post-processing: 1-10s (upscaling, effects)                            │
│                                                                              │
│  Total: 20s (cached, warm) to 5+ minutes (cold, complex workflow)           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Performance Targets#

Scenario Target Latency Acceptable Notes
Preview (low quality) <10s <20s Low steps, small resolution
Standard generation <30s <60s 30 steps, 1024x1024
High quality <60s <120s 50+ steps, refinement
Upscaling <30s <60s AI upscaler, 2-4x
Complex workflow <120s <300s Multi-stage, ControlNet

Client-Side Optimization#

Rate Limiting Configuration#

Configure rate limiting to avoid hitting provider limits:

typescript
import { RunComfyProvider } from '@oshun/comfy-provider';

const provider = new RunComfyProvider({
  apiKey: process.env.RUNCOMFY_API_KEY,

  // Rate limiting
  maxConcurrentJobs: 5, // Max parallel jobs
  queueSizeLimit: 100, // Max queued jobs
  enableQueue: true, // Enable local queue

  // Retry policy
  retryAttempts: 3, // Max retries
  retryBaseDelayMs: 1000, // Base delay (exponential backoff)

  // Timeouts
  defaultTimeoutMs: 300000, // 5 minute default
  pollingIntervalMs: 2000, // Poll every 2 seconds
  maxPollingAttempts: 150, // 5 min max polling
});

Exponential Backoff#

The provider uses exponential backoff with jitter:

typescript
// Built-in retry calculation
function calculateDelay(attempt: number, baseDelay: number): number {
  // Exponential: 1s, 2s, 4s, ...
  const exponentialDelay = baseDelay * Math.pow(2, attempt);

  // Add jitter (0-50% of delay)
  const jitter = exponentialDelay * Math.random() * 0.5;

  // Cap at 30 seconds
  return Math.min(exponentialDelay + jitter, 30000);
}

Circuit Breaker#

Prevent cascade failures with circuit breaker pattern:

typescript
interface CircuitBreakerConfig {
  failureThreshold: number; // Failures before opening (default: 5)
  successThreshold: number; // Successes to close (default: 2)
  timeout: number; // Time before half-open (default: 30000ms)
}

// The provider implements this internally
// Circuit states: CLOSED → OPEN → HALF_OPEN → CLOSED

Connection Pooling#

Reuse HTTP connections for better performance:

typescript
// Provider automatically uses connection pooling
// For custom clients, use keep-alive:
import { Agent } from 'https';

const agent = new Agent({
  keepAlive: true,
  keepAliveMsecs: 30000,
  maxSockets: 10,
  maxFreeSockets: 5,
});

const client = new ComfyClient({
  httpAgent: agent,
});

Workflow Optimization#

Reduce Sampling Steps#

Balance quality vs. speed:

Steps Quality Time Use Case
10-15 Draft Fast Previews, iteration
20-30 Good Medium Standard generation
40-50 High Slow Final output
50+ Diminishing returns Very slow Rarely needed
typescript
// Preview workflow
const previewConfig = {
  steps: 15,
  cfg: 7.0,
  sampler: 'euler', // Fastest
};

// Production workflow
const productionConfig = {
  steps: 30,
  cfg: 7.5,
  sampler: 'dpmpp_2m_sde', // Higher quality
};

Choose Efficient Samplers#

Sampler performance comparison:

Sampler Steps Needed Speed Quality
euler 30-40 Fastest Good
euler_ancestral 30-40 Fast Good (varied)
dpm_2 20-30 Medium Better
dpmpp_2m 20-30 Fast Better
dpmpp_2m_sde 20-30 Medium Best
dpmpp_3m_sde 15-25 Slow Best
ddim 30-50 Fast Good
uni_pc 15-25 Fast Better
typescript
// For speed
{ sampler_name: 'euler', steps: 30 }

// For quality
{ sampler_name: 'dpmpp_2m_sde', scheduler: 'karras', steps: 25 }

// For consistency (deterministic)
{ sampler_name: 'ddim', steps: 30 }

Optimize Resolution#

Match resolution to model capabilities:

typescript
// SDXL optimal resolutions (1024px base)
const SDXL_RESOLUTIONS = {
  square: { width: 1024, height: 1024 },
  portrait: { width: 832, height: 1216 },
  landscape: { width: 1216, height: 832 },
  wide: { width: 1344, height: 768 },
  tall: { width: 768, height: 1344 },
};

// SD 1.5 optimal resolutions (512px base)
const SD15_RESOLUTIONS = {
  square: { width: 512, height: 512 },
  portrait: { width: 448, height: 640 },
  landscape: { width: 640, height: 448 },
};

// For performance, generate at lower resolution, then upscale
const efficientWorkflow = {
  generateAt: { width: 768, height: 768 },
  upscaleTo: { width: 2048, height: 2048 },
};

Batch Processing#

Generate multiple images efficiently:

typescript
// Single batch (more efficient)
{
  "class_type": "EmptyLatentImage",
  "inputs": {
    "width": 1024,
    "height": 1024,
    "batch_size": 4  // Generate 4 images in one pass
  }
}

// vs. 4 separate jobs (less efficient)
// Each job has overhead: queue, cold start, model loading

Batch size recommendations:

GPU VRAM Max Batch (1024px) Max Batch (512px)
8GB 1 2-4
16GB 2-4 4-8
24GB 4-6 8-12
40GB 6-10 12-16

Model Loading Optimization#

Minimize model switching:

typescript
// Bad: Different model per job (constant loading)
const jobs = [
  { model: 'modelA', prompt: '...' },
  { model: 'modelB', prompt: '...' },
  { model: 'modelA', prompt: '...' }, // Reloads modelA
];

// Good: Group by model (minimize loads)
const jobs = [
  { model: 'modelA', prompt: '...' },
  { model: 'modelA', prompt: '...' }, // Already loaded
  { model: 'modelB', prompt: '...' },
];

// Best: Batch with same model
const batch = {
  model: 'modelA',
  prompts: ['...', '...', '...'],
  batch_size: 3,
};

Provider Configuration#

RunComfy Configuration#

typescript
const runcomfyConfig: RunComfyConfig = {
  apiKey: process.env.RUNCOMFY_API_KEY,

  // Performance tuning
  maxConcurrentJobs: 5,
  defaultTimeoutMs: 300000,
  pollingIntervalMs: 2000,
  maxPollingAttempts: 150,

  // Retry policy
  retryAttempts: 3,
  retryBaseDelayMs: 1000,

  // Queue management
  enableQueue: true,
  queueSizeLimit: 100,
};

RunPod Configuration#

typescript
const runpodConfig: RunPodConfig = {
  apiKey: process.env.RUNPOD_API_KEY,
  endpointId: process.env.RUNPOD_ENDPOINT_ID,

  // Worker scaling
  minWorkers: 0, // Scale to zero when idle
  maxWorkers: 5, // Maximum concurrent workers
  idleTimeout: 120, // Seconds before worker shutdown

  // Request handling
  executionTimeout: 300,
  retryOnFail: true,
  maxRetries: 2,
};

Multi-Provider Strategy#

typescript
import { ComfyCloudProvider, RoutingStrategy } from '@oshun/comfy-cloud';

const provider = new ComfyCloudProvider({
  providers: ['runcomfy', 'runpod', 'modal'],

  // Routing strategy
  routingStrategy: 'cost_optimized', // or 'latency_optimized', 'reliability'

  // Fallback configuration
  enableFailover: true,
  maxFailoverAttempts: 2,

  // Budget management
  dailyBudget: 50, // USD
  monthlyBudget: 500, // USD
  criticalJobReserve: 10, // % reserved for high-priority

  // Warm instance pool
  warmPoolSize: 2,
  warmPoolRefreshInterval: 300000, // 5 minutes
});

Routing Strategies:

Strategy Optimizes For Trade-off
cost_optimized Lowest cost May have higher latency
latency_optimized Fastest response Higher cost
reliability Highest success rate Variable cost
round_robin Even distribution No optimization
failover_only Primary provider Fallback on failure

Scaling Strategies#

Horizontal Scaling#

Scale workers based on queue depth:

typescript
// RunPod auto-scaling configuration
const scalingPolicy = {
  // Queue-based scaling
  scaleUpThreshold: 5, // Queue depth to trigger scale-up
  scaleDownThreshold: 1, // Queue depth to trigger scale-down

  // Worker limits
  minWorkers: 0,
  maxWorkers: 10,

  // Cooldowns
  scaleUpCooldown: 60, // Seconds between scale-ups
  scaleDownCooldown: 300, // Seconds between scale-downs

  // Worker warm-up
  warmUpTime: 30, // Seconds for worker to be ready
};

Warm Instance Pool#

Keep instances warm to avoid cold starts:

typescript
class WarmInstancePool {
  private pool: Map<string, InstanceInfo> = new Map();
  private minPoolSize: number = 2;

  async maintainPool(): Promise<void> {
    const currentSize = this.pool.size;

    if (currentSize < this.minPoolSize) {
      // Launch warm instances
      const needed = this.minPoolSize - currentSize;
      await this.launchInstances(needed);
    }

    // Refresh instances approaching timeout
    for (const [id, info] of this.pool) {
      if (info.idleTime > this.refreshThreshold) {
        await this.refreshInstance(id);
      }
    }
  }

  async getWarmInstance(): Promise<string | undefined> {
    for (const [id, info] of this.pool) {
      if (info.status === 'ready') {
        info.status = 'in_use';
        return id;
      }
    }
    return undefined; // No warm instance available
  }
}

Request Prioritization#

Prioritize critical jobs:

typescript
interface PriorityQueue {
  high: JobRequest[]; // UI-blocking, user waiting
  normal: JobRequest[]; // Standard requests
  low: JobRequest[]; // Background tasks, batch jobs
}

class PrioritizedJobQueue {
  private queues: PriorityQueue = { high: [], normal: [], low: [] };

  enqueue(job: JobRequest): void {
    const priority = job.priority || 'normal';
    this.queues[priority].push(job);
  }

  dequeue(): JobRequest | undefined {
    // High priority first
    if (this.queues.high.length > 0) {
      return this.queues.high.shift();
    }
    if (this.queues.normal.length > 0) {
      return this.queues.normal.shift();
    }
    return this.queues.low.shift();
  }
}

Caching#

Workflow Template Caching#

Cache compiled workflows:

typescript
const workflowCache = new Map<string, CompiledWorkflow>();
const CACHE_TTL = 3600000; // 1 hour

interface CompiledWorkflow {
  workflow: ComfyWorkflow;
  compiledAt: number;
  hash: string;
}

function getCachedWorkflow(templateId: string): ComfyWorkflow | null {
  const cached = workflowCache.get(templateId);

  if (cached && Date.now() - cached.compiledAt < CACHE_TTL) {
    return structuredClone(cached.workflow);
  }

  return null;
}

function cacheWorkflow(templateId: string, workflow: ComfyWorkflow): void {
  workflowCache.set(templateId, {
    workflow: structuredClone(workflow),
    compiledAt: Date.now(),
    hash: computeHash(workflow),
  });
}

Model Availability Caching#

Cache model discovery results:

typescript
class ModelAvailabilityCache {
  private cache: Map<string, ModelInfo[]> = new Map();
  private ttl: number = 300000; // 5 minutes

  async getAvailableModels(provider: string): Promise<ModelInfo[]> {
    const cached = this.cache.get(provider);

    if (cached) {
      return cached;
    }

    const models = await this.fetchModels(provider);
    this.cache.set(provider, models);

    // Auto-expire
    setTimeout(() => this.cache.delete(provider), this.ttl);

    return models;
  }
}

Output Caching#

Cache generated outputs for identical requests:

typescript
import { createHash } from 'crypto';

class OutputCache {
  private redis: Redis;
  private ttl: number = 86400; // 24 hours

  private computeKey(request: GenerationRequest): string {
    const normalized = {
      prompt: request.prompt.trim().toLowerCase(),
      negativePrompt: request.negativePrompt?.trim().toLowerCase(),
      model: request.model,
      width: request.width,
      height: request.height,
      steps: request.steps,
      cfg: request.cfg,
      seed: request.seed,
      sampler: request.sampler,
    };

    const hash = createHash('sha256')
      .update(JSON.stringify(normalized))
      .digest('hex');

    return `comfyui:output:${hash}`;
  }

  async get(request: GenerationRequest): Promise<string | null> {
    // Only cache deterministic requests (fixed seed)
    if (request.seed === -1) {
      return null;
    }

    const key = this.computeKey(request);
    return this.redis.get(key);
  }

  async set(request: GenerationRequest, outputUrl: string): Promise<void> {
    if (request.seed === -1) {
      return; // Don't cache random results
    }

    const key = this.computeKey(request);
    await this.redis.setex(key, this.ttl, outputUrl);
  }
}

Cost Optimization#

GPU Selection#

Choose appropriate GPU for workload:

GPU VRAM Cost/hr Best For
RTX 4090 24GB $0.50 SD 1.5, fast SDXL
A40 48GB $0.86 SDXL, batch processing
A100 40GB 40GB $1.58 Flux, large batches
A100 80GB 80GB $2.00 Training, extreme batches
typescript
function selectGPU(requirements: JobRequirements): string {
  const { model, batchSize, resolution } = requirements;

  // Calculate VRAM needed
  const vramNeeded = estimateVRAM(model, batchSize, resolution);

  if (vramNeeded <= 8) return 'rtx_4090';
  if (vramNeeded <= 24) return 'a40';
  if (vramNeeded <= 40) return 'a100_40gb';
  return 'a100_80gb';
}

function estimateVRAM(
  model: string,
  batchSize: number,
  resolution: { width: number; height: number }
): number {
  const baseVRAM: Record<string, number> = {
    'sd_1.5': 4,
    'sd_2.1': 5,
    sdxl: 8,
    flux: 20,
  };

  const base = baseVRAM[model] || 8;
  const resolutionFactor =
    (resolution.width * resolution.height) / (1024 * 1024);
  const batchFactor = 1 + (batchSize - 1) * 0.5;

  return base * resolutionFactor * batchFactor;
}

Idle Timeout Optimization#

Configure timeouts based on traffic patterns:

typescript
interface TrafficPattern {
  pattern: 'continuous' | 'bursty' | 'scheduled' | 'infrequent';
  idleTimeout: number; // seconds
  minWorkers: number;
}

const TRAFFIC_CONFIGS: Record<string, TrafficPattern> = {
  production: {
    pattern: 'continuous',
    idleTimeout: 120,
    minWorkers: 1,
  },
  staging: {
    pattern: 'bursty',
    idleTimeout: 60,
    minWorkers: 0,
  },
  development: {
    pattern: 'infrequent',
    idleTimeout: 30,
    minWorkers: 0,
  },
};

Cost Tracking#

typescript
interface CostRecord {
  jobId: string;
  provider: string;
  gpu: string;
  executionTimeSeconds: number;
  cost: number;
  timestamp: Date;
}

class CostTracker {
  private records: CostRecord[] = [];

  async recordJob(job: CompletedJob): Promise<void> {
    const pricePerSecond = GPU_PRICING[job.gpu];
    const cost = job.executionTimeSeconds * pricePerSecond;

    const record: CostRecord = {
      jobId: job.id,
      provider: job.provider,
      gpu: job.gpu,
      executionTimeSeconds: job.executionTimeSeconds,
      cost,
      timestamp: new Date(),
    };

    this.records.push(record);
    await this.persistRecord(record);
  }

  getDailyCost(): number {
    const today = new Date().toDateString();
    return this.records
      .filter((r) => r.timestamp.toDateString() === today)
      .reduce((sum, r) => sum + r.cost, 0);
  }

  getMonthlyCost(): number {
    const thisMonth = new Date().getMonth();
    return this.records
      .filter((r) => r.timestamp.getMonth() === thisMonth)
      .reduce((sum, r) => sum + r.cost, 0);
  }
}

Monitoring and Metrics#

Key Metrics#

typescript
interface ComfyUIMetrics {
  // Latency
  totalLatencyMs: number;
  queueTimeMs: number;
  executionTimeMs: number;
  networkTimeMs: number;

  // Throughput
  jobsPerMinute: number;
  imagesPerMinute: number;

  // Success rates
  successRate: number;
  retryRate: number;
  failureRate: number;

  // Resource usage
  activeWorkers: number;
  queueDepth: number;
  warmInstances: number;

  // Cost
  costPerJob: number;
  dailyCost: number;
  projectedMonthlyCost: number;
}

Prometheus Metrics#

typescript
import { Registry, Counter, Histogram, Gauge } from 'prom-client';

const registry = new Registry();

// Job metrics
const jobsTotal = new Counter({
  name: 'comfyui_jobs_total',
  help: 'Total ComfyUI jobs',
  labelNames: ['provider', 'status', 'model'],
  registers: [registry],
});

const jobDuration = new Histogram({
  name: 'comfyui_job_duration_seconds',
  help: 'ComfyUI job duration',
  labelNames: ['provider', 'model'],
  buckets: [5, 10, 20, 30, 60, 120, 300],
  registers: [registry],
});

const queueDepth = new Gauge({
  name: 'comfyui_queue_depth',
  help: 'Current queue depth',
  labelNames: ['provider'],
  registers: [registry],
});

// Usage
function recordJobCompletion(job: CompletedJob): void {
  jobsTotal.labels(job.provider, 'success', job.model).inc();
  jobDuration.labels(job.provider, job.model).observe(job.duration);
}

Health Checks#

typescript
interface HealthCheckResult {
  healthy: boolean;
  providers: Record<string, ProviderHealth>;
  queue: QueueHealth;
  cache: CacheHealth;
}

interface ProviderHealth {
  status: 'healthy' | 'degraded' | 'unhealthy';
  latencyMs: number;
  errorRate: number;
  activeWorkers: number;
  lastError?: string;
}

async function healthCheck(): Promise<HealthCheckResult> {
  const providers: Record<string, ProviderHealth> = {};

  for (const provider of ['runcomfy', 'runpod']) {
    try {
      const start = Date.now();
      const status = await checkProvider(provider);
      const latency = Date.now() - start;

      providers[provider] = {
        status: status.healthy ? 'healthy' : 'degraded',
        latencyMs: latency,
        errorRate: status.errorRate,
        activeWorkers: status.activeWorkers,
      };
    } catch (error) {
      providers[provider] = {
        status: 'unhealthy',
        latencyMs: -1,
        errorRate: 1,
        activeWorkers: 0,
        lastError: error.message,
      };
    }
  }

  return {
    healthy: Object.values(providers).some((p) => p.status === 'healthy'),
    providers,
    queue: await checkQueue(),
    cache: await checkCache(),
  };
}

Troubleshooting Performance#

High Latency#

Symptoms: Jobs taking longer than expected

Diagnosis:

typescript
// Break down latency components
const metrics = await provider.getJobMetrics(jobId);

console.log('Queue time:', metrics.queueTimeMs);
console.log('Cold start:', metrics.coldStartMs);
console.log('Model load:', metrics.modelLoadMs);
console.log('Execution:', metrics.executionMs);
console.log('Upload:', metrics.uploadMs);

Solutions:

Component Issue Solution
Queue time High demand Scale workers, prioritize
Cold start No warm instances Increase min workers
Model load Frequent switching Batch by model
Execution Too many steps Optimize workflow
Upload Large files Compress, use CDN

High Error Rate#

Symptoms: Jobs failing frequently

Diagnosis:

typescript
const analytics = await provider.getAnalytics();

console.log('Error distribution:');
for (const [code, count] of Object.entries(analytics.errorDistribution)) {
  console.log(`  ${code}: ${count}`);
}

Common Errors and Solutions:

Error Cause Solution
TIMEOUT Job too slow Reduce steps, resolution
RATE_LIMITED Too many requests Reduce concurrency
INSUFFICIENT_CREDITS Budget exhausted Add credits
MODEL_NOT_FOUND Invalid model Check model availability
WORKFLOW_INVALID Bad workflow Validate before submit
OOM Out of memory Reduce batch size

Memory Issues#

Symptoms: OOM errors, worker crashes

Solutions:

typescript
// Reduce memory usage
const lowMemoryConfig = {
  // Smaller batches
  batchSize: 1,

  // Lower resolution
  width: 768,
  height: 768,

  // Use FP16 VAE
  vae: 'sdxl_vae_fp16.safetensors',

  // Limit LoRAs
  maxLoras: 2,

  // Use tiled VAE for large images
  tiledVAE: true,
  tileSize: 512,
};

Queue Backlog#

Symptoms: Long wait times, growing queue

Solutions:

typescript
// Scale up workers
await provider.scaleWorkers({
  minWorkers: 2,
  maxWorkers: 10,
});

// Enable priority queue
const job = await provider.submitJob({
  workflow,
  priority: 1, // Higher priority
});

// Reject low-priority jobs during high load
if (queueDepth > threshold) {
  throw new Error('Service busy, try again later');
}