> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Scaling & Resource Management

> Scale a Livepeer Gateway - session limit benchmarking, GPU resource management, when to split video and AI workloads onto separate nodes, and horizontal scaling patterns.

export const StyledStep = ({title, icon, titleSize = 'h3', iconColor = null, titleColor = null, children, className = '', style = {}, ...rest}) => {
  const styledTitle = titleColor ? <span style={{
    color: titleColor
  }}>{title}</span> : title;
  return <Step title={styledTitle} icon={icon} iconColor={iconColor || undefined} titleSize={titleSize} className={className} style={style} {...rest}>
      {children}
    </Step>;
};

export const StyledSteps = ({children, iconColor, titleColor, lineColor, iconSize = '24px', className = '', style = {}, ...rest}) => {
  const resolvedIconColor = iconColor || 'var(--accent-dark, #18794E)';
  const resolvedTitleColor = titleColor || 'var(--lp-color-accent)';
  const resolvedLineColor = lineColor || 'var(--lp-color-accent)';
  return <div className={['docs-styled-steps', className].filter(Boolean).join(' ')} style={style} {...rest}>
      <style>{`
        .docs-styled-steps .steps > div > div.absolute > div {
          background-color: ${resolvedIconColor};
        }
        .docs-styled-steps .steps > div > div.w-full > p {
          color: ${resolvedTitleColor};
        }
        .docs-styled-steps .steps > div > div.absolute.w-px {
          background-color: ${resolvedLineColor};
        }
        .docs-styled-steps .steps > div:last-child > div.absolute.w-px::after {
          content: '';
          position: absolute;
          bottom: 0;
          left: 50%;
          transform: translateX(-50%);
          width: 6px;
          height: 6px;
          background-color: ${resolvedLineColor};
          transform: translateX(-50%) rotate(45deg);
        }
      `}</style>
      <div>
        <Steps>{children}</Steps>
      </div>
    </div>;
};

export const CenteredContainer = ({children, maxWidth = "800px", padding = "0", preset = "default", width = "", minWidth = "", marginRight = "", marginBottom = "", textAlign = "", style = {}, className = "", ...rest}) => {
  const presets = {
    default: {},
    fitContent: {
      width: "fit-content",
      minWidth: "fit-content"
    },
    readable70: {
      width: "70%",
      minWidth: "fit-content"
    },
    readable80: {
      width: "80%",
      minWidth: "fit-content"
    },
    readable90: {
      width: "90%"
    },
    wide900: {
      maxWidth: "900px"
    }
  };
  const presetStyle = presets[preset] || presets.default;
  return <div className={className} style={{
    maxWidth: presetStyle.maxWidth || maxWidth,
    margin: "0 auto",
    padding: padding,
    ...presetStyle.width ? {
      width: presetStyle.width
    } : {},
    ...presetStyle.minWidth ? {
      minWidth: presetStyle.minWidth
    } : {},
    ...width ? {
      width
    } : {},
    ...minWidth ? {
      minWidth
    } : {},
    ...marginRight ? {
      marginRight
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...textAlign ? {
      textAlign
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const TableCell = ({children, align = "left", header = false, style = {}, className = "", ...rest}) => {
  const Component = header ? "th" : "td";
  return <Component className={className} style={{
    padding: "0.75rem 1rem",
    textAlign: align,
    border: header ? "none" : "1px solid var(--lp-color-border-default)",
    ...style
  }} {...rest}>
      {children}
    </Component>;
};

export const TableRow = ({children, header = false, hover = false, style = {}, className = "", ...rest}) => {
  const rowId = `table-row-${Math.random().toString(36).substr(2, 9)}`;
  return <>
      {hover && <style>{`
          #${rowId}:hover {
            background-color: var(--lp-color-bg-card);
          }
        `}</style>}
      <tr id={rowId} className={className} style={{
    ...header && ({
      backgroundColor: "var(--lp-color-accent-strong)",
      color: "var(--lp-color-on-accent)",
      fontWeight: "bold"
    }),
    ...style
  }} {...rest}>
        {children}
      </tr>
    </>;
};

export const StyledTable = ({children, variant = "default", style = {}, className = "", ...rest}) => {
  const wrapperVariants = {
    default: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "var(--lp-color-bg-card)",
      overflow: "hidden"
    },
    bordered: {
      border: "2px solid var(--lp-color-accent)",
      backgroundColor: "var(--lp-color-bg-page)",
      overflow: "hidden"
    },
    minimal: {
      border: "none",
      backgroundColor: "transparent",
      overflow: "visible"
    }
  };
  return <div data-docs-styled-table-shell className={className} style={{
    width: "100%",
    padding: 0,
    margin: 0,
    ...wrapperVariants[variant],
    ...style
  }} {...rest}>
      <table data-docs-styled-table style={{
    width: "100%",
    borderCollapse: "collapse",
    borderSpacing: 0,
    margin: 0,
    backgroundColor: "transparent"
  }}>
        {children}
      </table>
    </div>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

<CenteredContainer preset="readable90" minWidth="fit-content">
  <Note>
    Session limits and GPU management apply primarily to Orchestrators running compute locally.
    <br /> A pure routing Gateway (off-chain AI, no local GPU) is limited mainly by network throughput and horizontal instance count.
    <br /> Jump to [Horizontal Scaling](#horizontal-scaling) if that describes the setup.
  </Note>
</CenteredContainer>

<CustomDivider style={{margin: "-1rem 0 -1rem 0"}} />

A Gateway routes jobs to Orchestrators instead of processing them locally, but it still has resource limits: session capacity, network throughput, and (for Dual Gateways) the GPU resources shared between video and AI workloads.
<br /> how to measure those limits, decide when they have been reached, and scale in the right direction.

## Scaling Signals

Before adjusting anything, confirm there is actually a capacity problem. These are the reliable indicators.

<AccordionGroup>
  <Accordion title="Session limit reached" icon="gauge-high">
    The Prometheus metric `livepeer_current_sessions_total` approaching `livepeer_max_sessions_total` means the Gateway is at capacity and will start rejecting new sessions. This is the clearest signal.

    ```bash icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    curl http://localhost:8935/metrics | grep livepeer_current_sessions
    ```
  </Accordion>

  <Accordion title="GPU memory pressure" icon="microchip">
    For Dual Gateways, the `/hardware/stats` endpoint reports GPU utilisation and memory:

    ```bash icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    curl http://localhost:8935/hardware/stats
    ```

    If VRAM usage is consistently above 85%, the Gateway is at risk of OOM failures on new model loads (AI) or performance degradation on concurrent transcoding segments (video).
  </Accordion>

  <Accordion title="Increasing latency under load" icon="clock">
    Rising transcoding latency or AI inference latency under load, combined with high Orchestrator swap rates, suggests the Orchestrators being routed to are also under pressure. This is an Orchestrator-side scaling problem. See <LinkArrow href="/v2/gateways/guides/advanced-operations/orchestrator-selection" label="Orchestrator Selection" newline={false} /> for expanding the Orchestrator pool.
  </Accordion>

  <Accordion title="Client rejections" icon="ban">
    Errors with `OrchestratorCapped` in the Gateway log indicate that a downstream Orchestrator has hit its own session limit and rejected the job. Either expand the Orchestrator pool or negotiate higher capacity with preferred Orchestrators.
  </Accordion>
</AccordionGroup>

<CustomDivider style={{margin: "0 0 -2rem 0"}} />

<StyledSteps>
  <StyledStep icon="circle-stop">
    ### Session Limits

    Session limits define how many concurrent jobs the Gateway (or its local Orchestrator, for dual setups) accepts. The default is 10.

    ```bash icon="terminal" -maxSessions theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    -maxSessions 20
    ```

    Setting this too high causes performance degradation or OOM failures. Setting it too low wastes available capacity. The right value comes from benchmarking.
  </StyledStep>

  <StyledStep icon="heart-pulse">
    ### Benchmarking

    `livepeer_bench` tests transcoding throughput for a range of concurrent session counts. Run this on the machine that will perform local transcoding before setting `-maxSessions` in production.

    ```bash icon="terminal" livepeer_bench theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    # Download the standard transcoding options file
    wget https://raw.githubusercontent.com/livepeer/go-livepeer/master/cmd/livepeer_bench/transcodingOptions.json

    # Run the benchmark across 1 to 20 concurrent sessions
    for i in {1..20}; do
        ./livepeer_bench \
            -in bbb/source.m3u8 \
            -transcodingOptions transcodingOptions.json \
            -nvidia 0 \
            -concurrentSessions $i |& grep "Duration Ratio" >> bench.log
    done

    cat bench.log
    ```

    Read the output: find the last line where the real-time duration ratio is at or below 0.8. That concurrent session count is the hardware limit. Leave a 20% buffer for upload and download overhead.

    ```text icon="terminal" Output Example theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    Concurrent sessions 12: Duration Ratio 0.79  <- last line at or below 0.8
    Concurrent sessions 13: Duration Ratio 0.88
    Concurrent sessions 14: Duration Ratio 0.97
    ```

    In this example, set `-maxSessions 12`.

    For machines with multiple identical GPUs, benchmark one GPU and multiply. For machines with different GPU models, benchmark each separately and use the lower value as the bottleneck.

    <Tip>
      Bandwidth can be a tighter limit than compute. Each 1080p stream requires approximately 4-8 Mbps inbound and 12-24 Mbps outbound (for a 4-rung ABR ladder). If bandwidth is the bottleneck, it sets a lower ceiling than the hardware benchmark.
    </Tip>
  </StyledStep>

  <StyledStep icon="tachograph-digital">
    ### GPU Management

    For Dual Gateways, video transcoding and AI inference compete for the same GPU.

    ```bash icon="terminal" GPU Stats theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    # GPU model, VRAM, and system specs
    curl http://localhost:8935/hardware/info

    # Live GPU utilisation, memory, temperature
    curl http://localhost:8935/hardware/stats
    ```

    VRAM allocation between video transcoding and AI inference is managed by the Orchestrator, not the Gateway. Video transcoding (NVENC/NVDEC) uses dedicated hardware encoder/decoder blocks on NVIDIA GPUs, which do not compete directly with CUDA cores used for AI inference. However, VRAM is shared, and large AI models can exhaust available memory.

    As a rule of thumb:

    * Reserve at least 8 GB VRAM per concurrent AI inference model
    * Keep video transcoding sessions below the benchmarked hardware limit even when running AI jobs simultaneously
    * Monitor temperature: sustained GPU temperature above 85 degrees C indicates thermal throttling
  </StyledStep>

  <StyledStep icon="scale-balanced">
    ### Split or Share?

    If a Dual Gateway runs video + AI on one node, the central decision is when to split those workloads onto separate nodes.

    <Tabs>
      <Tab title="Decision Guide" icon="ballot">
        <StyledTable>
          <TableRow header>
            <TableCell header>Factor</TableCell>
            <TableCell header>Stay on single node</TableCell>
            <TableCell header>Split onto separate nodes</TableCell>
          </TableRow>

          <TableRow>
            <TableCell>Load</TableCell>
            <TableCell>Low to medium, predictable</TableCell>
            <TableCell>High, variable, or growing</TableCell>
          </TableRow>

          <TableRow>
            <TableCell>SLA requirements</TableCell>
            <TableCell>Best-effort</TableCell>
            <TableCell>Committed latency or uptime</TableCell>
          </TableRow>

          <TableRow>
            <TableCell>GPU VRAM</TableCell>
            <TableCell>Model fits with transcoding headroom</TableCell>
            <TableCell>Models exhaust VRAM under load</TableCell>
          </TableRow>

          <TableRow>
            <TableCell>Operational complexity</TableCell>
            <TableCell>Prefer simplicity</TableCell>
            <TableCell>Can manage two nodes</TableCell>
          </TableRow>

          <TableRow>
            <TableCell>Cost</TableCell>
            <TableCell>Minimise hardware spend</TableCell>
            <TableCell>Willing to pay for isolation</TableCell>
          </TableRow>
        </StyledTable>

        The practical trigger for splitting is usually VRAM contention (AI models OOM when video load spikes) or SLA pressure (customers require guaranteed latency for one workload that the other interferes with).
      </Tab>

      <Tab title="Split Architecture" icon="split">
        Once split, run two separate go-livepeer processes - one configured as a Video Gateway, one as an AI Gateway - each with its own Orchestrator list:

        ```bash icon="terminal" Split Settings theme={"theme":{"light":"github-light","dark":"dark-plus"}}
        # Video Gateway (on-chain, port 8935, RTMP 1935)
        livepeer -gateway \
            -network arbitrum-one-mainnet \
            -rtmpAddr 0.0.0.0:1935 \
            -httpAddr 0.0.0.0:8935 \
            -orchAddr <video-orchestrators>

        # AI Gateway (off-chain, port 8935 on separate host)
        livepeer -gateway \
            -httpAddr 0.0.0.0:8935 \
            -orchAddr <ai-orchestrators> \
            -httpIngest
        ```

        The middleware or load balancer routes incoming requests to the correct Gateway based on job type.
      </Tab>
    </Tabs>
  </StyledStep>

  <StyledStep icon="arrows-left-right-to-line">
    ### Horizontal Scaling

    ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#18794E', 'primaryTextColor': '#fff', 'primaryBorderColor': '#3CB540', 'lineColor': '#3CB540', 'mainBkg': '#18794E', 'nodeBorder': '#3CB540', 'clusterBkg': 'transparent', 'clusterBorder': '#3CB540', 'titleColor': '#3CB540', 'edgeLabelBackground': 'transparent', 'textColor': '#3CB540', 'nodeTextColor': '#fff'}}}%%
    flowchart LR
        subgraph Scale["Scaling path"]
        direction LR
        S1["1. Vertical\nBigger GPU"] --> S2["2. Split\nSeparate video + AI nodes"] --> S3["3. Horizontal\nMultiple instances per type"]
        end
    ```

    **Off-chain AI Gateways** scale horizontally without special configuration. They are stateless: no ETH key, no session state. Add instances behind a standard HTTP load balancer and use the `/health` endpoint for health checks.

    ```yaml icon="code" /health yaml theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    # Example: three AI Gateway instances behind nginx
    upstream ai_gateways {
        server 10.0.0.1:8935;
        server 10.0.0.2:8935;
        server 10.0.0.3:8935;
    }

    server {
        location / {
            proxy_pass http://ai_gateways;
        }
    }
    ```

    **Video Gateways** require sticky sessions for streaming continuity. A live RTMP stream cannot be moved between Gateway instances mid-session. Configure the load balancer to use IP-hash or cookie-based session affinity for RTMP connections. HTTP API requests (non-streaming) can be round-robined normally.

    **Dual Gateways:** Split into separate video and AI instances before scaling horizontally. Trying to horizontally scale a Dual node adds complexity without solving the resource contention problem.
  </StyledStep>
</StyledSteps>

<CustomDivider style={{margin: "0 0 -2rem 0"}} />

## Capacity Planning

<AccordionGroup>
  <Accordion title="Video transcoding" icon="video">
    Per GPU, the practical limit for 1080p to 720p/480p/360p transcoding is approximately 8-12 concurrent sessions on a modern NVIDIA T4, and 15-25 on an RTX 3080 or equivalent. Run `livepeer_bench` on the specific hardware to get a precise number.

    Multiply by the number of GPUs in the Orchestrator pool to get total network capacity. The Gateway can route to all of them.

    For deposit sizing: video transcoding payments are per-pixel. Estimate the expected pixel throughput (resolution x frame rate x concurrent sessions x hours per day) and size the Arbitrum deposit to cover at least 24 hours of expected traffic.
  </Accordion>

  <Accordion title="AI inference" icon="brain">
    AI inference capacity depends entirely on the model and the Orchestrators in the pool. FLUX.1-dev requires approximately 12-16 GB VRAM per concurrent inference; smaller SD 1.5 models can fit in 6 GB.

    For off-chain AI Gateways, the bottleneck is the number of AI-capable Orchestrators available. Expand the `-orchAddr` list or use a discovery URL that returns a larger pool.

    Set a monitoring alert at 70% of observed capacity. This gives time to provision additional Orchestrators before hitting the ceiling.
  </Accordion>

  <Accordion title="Alert thresholds" icon="bell">
    <StyledTable>
      <TableRow header>
        <TableCell header>Metric</TableCell>
        <TableCell header>Warning</TableCell>
        <TableCell header>Critical</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>Session utilisation</TableCell>
        <TableCell>70% of `-maxSessions`</TableCell>
        <TableCell>90% of `-maxSessions`</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>GPU memory</TableCell>
        <TableCell>75% VRAM used</TableCell>
        <TableCell>90% VRAM used</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>GPU temperature</TableCell>
        <TableCell>80 degrees C</TableCell>
        <TableCell>87 degrees C</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>Orchestrator swap rate</TableCell>
        <TableCell>2x baseline</TableCell>
        <TableCell>5x baseline</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>ETH deposit balance</TableCell>
        <TableCell>20% remaining</TableCell>
        <TableCell>5% remaining</TableCell>
      </TableRow>
    </StyledTable>
  </Accordion>
</AccordionGroup>

<CustomDivider style={{margin: "0 0 -2rem 0"}} />

## Related Pages

<CardGroup cols={2}>
  <Card title="Orchestrator Selection" icon="server" href="/v2/gateways/guides/advanced-operations/orchestrator-selection">
    Expand the Orchestrator pool to increase available capacity.
  </Card>

  <Card title="Gateway Middleware" icon="layer-group" href="/v2/gateways/guides/advanced-operations/gateway-middleware">
    Route requests across scaled instances with middleware.
  </Card>

  <Card title="GPU Support" icon="microchip" href="/v2/gateways/resources/reference/go-livepeer/gpu-support">
    Tested GPU models and session limit data.
  </Card>

  <Card title="Monitoring Setup" icon="chart-line" href="/v2/gateways/guides/monitoring-and-tooling/monitoring-setup">
    Prometheus metrics and dashboards for scaling signals.
  </Card>
</CardGroup>
