WARNING

Root Cause Analysis: Why HTTP/2 Stream Reset (RST_STREAM) Happens

Quick Fix Summary

TL;DR

Increase server/client timeouts and implement proper stream lifecycle management.

RST_STREAM is an HTTP/2 frame that immediately terminates a single stream without affecting the connection. It's typically triggered by protocol violations, resource constraints, or application logic.

Diagnosis & Causes

  • Client cancels request before server completes response.
  • Server exceeds client's advertised flow control window.
  • Application error triggers premature stream termination.
  • Race condition between request cancellation and response.
  • Load balancer or proxy misconfiguration with HTTP/2.
  • Recovery Steps

    1

    Step 1: Enable HTTP/2 Debug Logging

    Configure detailed HTTP/2 logging to capture RST_STREAM frames with error codes.

    bash
    # Nginx
    http2_log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                          'h2_stream_id=$h2_stream_id h2_stream_error=$h2_stream_error';
    http2_log /var/log/nginx/h2.log main;
    # Apache
    LogLevel http2:trace2
    2

    Step 2: Analyze RST_STREAM Error Codes

    Decode the error code in the RST_STREAM frame to identify the specific violation.

    text
    NO_ERROR (0x0) = Graceful termination
    PROTOCOL_ERROR (0x1) = Unrecoverable protocol violation
    INTERNAL_ERROR (0x2) = Generic internal error
    FLOW_CONTROL_ERROR (0x3) = Flow control limits exceeded
    STREAM_CLOSED (0x5) = Frame received for closed stream
    FRAME_SIZE_ERROR (0x6) = Invalid frame size
    REFUSED_STREAM (0x7) = Stream refused before processing
    CANCEL (0x8) = Stream cancelled by application
    3

    Step 3: Implement Client-Side Retry Logic

    Handle REFUSED_STREAM errors with exponential backoff retries on idempotent requests.

    python
    import aiohttp
    import asyncio
    async def fetch_with_retry(url, max_retries=3):
        backoff = 1
        for attempt in range(max_retries):
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.get(url) as response:
                        return await response.text()
            except aiohttp.ClientError as e:
                if 'REFUSED_STREAM' in str(e) and attempt < max_retries - 1:
                    await asyncio.sleep(backoff)
                    backoff *= 2
                else:
                    raise
        raise Exception('Max retries exceeded')
    4

    Step 4: Tune Flow Control Windows

    Adjust initial window size and maximum frame size to prevent FLOW_CONTROL_ERROR.

    bash
    # Nginx - Increase initial flow control window
    http2_body_preread_size 256k;
    http2_recv_buffer_size 256k;
    # Apache - Configure HTTP/2 window size
    H2WindowSize 65535
    H2MaxDataFrameLen 16384
    # Node.js (http2 module)
    const server = http2.createSecureServer({
      settings: {
        initialWindowSize: 65535,
        maxFrameSize: 16384
      }
    });
    5

    Step 5: Validate Middleware HTTP/2 Compliance

    Ensure proxies, WAFs, and load balancers properly support HTTP/2 frame propagation.

    bash
    # Test HTTP/2 compliance with h2spec
    h2spec http2 -h your-server.com -p 443 -t -k
    # Check nginx proxy settings
    proxy_http_version 1.1; # ← WRONG, breaks HTTP/2
    proxy_http_version 2; # ← CORRECT
    # Verify ALPN negotiation
    openssl s_client -alpn h2 -connect your-server.com:443
    6

    Step 6: Monitor Stream Reset Patterns

    Create alerts for abnormal RST_STREAM rates using Prometheus metrics.

    go
    # Prometheus query for high RST_STREAM rate
    rate(nginx_http_requests_total{status="499"}[5m]) > 0.1
    # Go HTTP/2 server metrics
    import "golang.org/x/net/http2"
    var (
        streamResets = prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "http2_stream_resets_total",
                Help: "Total RST_STREAM frames sent",
            },
            []string{"error_code"},
        )
    )
    http2.ConfigureServer(server, &http2.Server{
        CountError: func(errType string) {
            streamResets.WithLabelValues(errType).Inc()
        }
    })

    Architect's Pro Tip

    "RST_STREAM with CANCEL (0x8) often indicates client-side fetch() API abortions in browsers - implement request debouncing and consider Request.signal for proper cleanup."

    Frequently Asked Questions

    Does RST_STREAM close the entire HTTP/2 connection?

    No, RST_STREAM only terminates a single stream. The connection remains active for other streams, unlike TCP RST which kills the entire connection.

    How does RST_STREAM differ from HTTP/1.1 connection close?

    HTTP/1.1 requires closing the entire TCP connection to cancel a request. HTTP/2's RST_STREAM is connection-efficient, resetting only the problematic stream while preserving multiplexed streams.

    Can RST_STREAM cause data loss?

    Yes, if sent mid-transmission. The reset stream's data is discarded. For critical data, implement application-level acknowledgments or use idempotent retry patterns with REFUSED_STREAM handling.

    Related HTTP Protocol Guides