Test HTTP/2 Max Concurrent Streams: Tools & Guide
Testing HTTP/2 max concurrent streams requires tools such as h2load, curl with HTTP/2 support, or custom scripts using the h2 Python library. Run h2load with flags like -n 100 -c 1 -m 200 to observe where your server enforces limits. Proper enforcement of SETTINGS_MAX_CONCURRENT_STREAMS defends against CVE-2023-44487 Rapid Reset attacks and ensures stable performance under load.
1. Introduction
Understanding and testing your server’s maximum concurrent stream configuration is critical for both performance tuning and security hardening against HTTP/2 attacks. This guide provides comprehensive tools and techniques to test the SETTINGS_MAX_CONCURRENT_STREAMS parameter on your web servers.
This article complements our previous guide on Testing Your Website for HTTP/2 Rapid Reset Vulnerabilities from a macOS. While that article focuses on the CVE-2023-44487 Rapid Reset attack, this guide helps you verify that your server properly enforces stream limits, which is a critical defense mechanism.
2. Why Test Stream Limits?
The SETTINGS_MAX_CONCURRENT_STREAMS setting determines how many concurrent requests a client can multiplex over a single HTTP/2 connection. Testing this limit is important because:
- Security validation: Confirms your server enforces reasonable stream limits
- Configuration verification: Ensures your settings match security recommendations (typically 100-128 streams)
- Performance tuning: Helps optimize the balance between throughput and resource consumption
- Attack surface assessment: Identifies if servers accept dangerously high stream counts
3. Understanding HTTP/2 Stream Limits
When an HTTP/2 connection is established, the server sends a SETTINGS frame that includes:
SETTINGS_MAX_CONCURRENT_STREAMS: 100 This tells the client the maximum number of concurrent streams allowed. A compliant client should respect this limit, but attackers will not.
3.1. Common Default Values
Web Servers:
- Nginx: 128 (configurable via
http2_max_concurrent_streams) - Apache: 100 (configurable via
H2MaxSessionStreams) - Caddy: 250 (configurable via
max_concurrent_streams) - LiteSpeed: 100 (configurable in admin panel)
Reverse Proxies and Load Balancers:
- HAProxy: No default limit (should be explicitly configured)
- Envoy: 100 (configurable via
max_concurrent_streams) - Traefik: 250 (configurable via
maxConcurrentStreams)
CDN and Cloud Services:
- CloudFlare: 128 (managed automatically)
- AWS ALB: 128 (managed automatically)
- Azure Front Door: 100 (managed automatically)
4. The Stream Limit Testing Script
The following Python script tests your server’s maximum concurrent streams using the h2 library. This script will:
- Connect to your HTTP/2 server
- Read the advertised
SETTINGS_MAX_CONCURRENT_STREAMSvalue - Attempt to open more streams than the advertised limit
- Verify that the server actually enforces the limit
- Provide detailed results and recommendations
4.1. Prerequisites
Install the required Python libraries:
pip3 install h2 hyper --break-system-packages Verify installation:
python3 -c "import h2; print(f'h2 version: {h2.__version__}')" 4.2. Complete Script
Save the following as http2_stream_limit_tester.py:
#!/usr/bin/env python3
"""
HTTP/2 Maximum Concurrent Streams Tester
Tests the SETTINGS_MAX_CONCURRENT_STREAMS limit on HTTP/2 servers
and attempts to exceed it to verify enforcement.
Usage:
python3 http2_stream_limit_tester.py --host example.com --port 443
Requirements:
pip3 install h2 hyper --break-system-packages
"""
import argparse
import socket
import ssl
import time
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
try:
from h2.connection import H2Connection
from h2.config import H2Configuration
from h2.events import (
RemoteSettingsChanged,
StreamEnded,
DataReceived,
StreamReset,
WindowUpdated,
SettingsAcknowledged,
ResponseReceived
)
from h2.exceptions import ProtocolError
except ImportError:
print("Error: h2 library not installed")
print("Install with: pip3 install h2 hyper --break-system-packages")
exit(1)
@dataclass
class StreamLimitTestResults:
"""Results from stream limit testing"""
advertised_max_streams: Optional[int] = None
actual_max_streams: int = 0
successful_streams: int = 0
failed_streams: int = 0
reset_streams: int = 0
enforcement_detected: bool = False
test_duration: float = 0.0
server_settings: Dict = field(default_factory=dict)
errors: List[str] = field(default_factory=list)
class HTTP2StreamLimitTester:
"""Test HTTP/2 server stream limits"""
def __init__(
self,
host: str,
port: int = 443,
path: str = "/",
use_tls: bool = True,
timeout: int = 30,
verbose: bool = False
):
self.host = host
self.port = port
self.path = path
self.use_tls = use_tls
self.timeout = timeout
self.verbose = verbose
self.socket: Optional[socket.socket] = None
self.h2_conn: Optional[H2Connection] = None
self.server_max_streams: Optional[int] = None
self.active_streams: Dict[int, dict] = {}
def connect(self) -> bool:
"""Establish connection to the server"""
try:
# Create socket
self.socket = socket.create_connection(
(self.host, self.port),
timeout=self.timeout
)
# Wrap with TLS if needed
if self.use_tls:
context = ssl.create_default_context()
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
# Set ALPN protocols for HTTP/2
context.set_alpn_protocols(['h2', 'http/1.1'])
self.socket = context.wrap_socket(
self.socket,
server_hostname=self.host
)
# Verify HTTP/2 was negotiated
negotiated_protocol = self.socket.selected_alpn_protocol()
if negotiated_protocol != 'h2':
raise Exception(f"HTTP/2 not negotiated. Got: {negotiated_protocol}")
if self.verbose:
print(f"TLS connection established (ALPN: {negotiated_protocol})")
# Initialize HTTP/2 connection
config = H2Configuration(client_side=True)
self.h2_conn = H2Connection(config=config)
self.h2_conn.initiate_connection()
# Send connection preface
self.socket.sendall(self.h2_conn.data_to_send())
# Receive server settings
self._receive_data()
if self.verbose:
print(f"HTTP/2 connection established to {self.host}:{self.port}")
return True
except Exception as e:
if self.verbose:
print(f"Connection failed: {e}")
return False
def _receive_data(self, timeout: Optional[float] = None) -> List:
"""Receive and process data from server"""
if timeout:
self.socket.settimeout(timeout)
else:
self.socket.settimeout(self.timeout)
events = []
try:
data = self.socket.recv(65536)
if not data:
return events
events_received = self.h2_conn.receive_data(data)
for event in events_received:
events.append(event)
if isinstance(event, RemoteSettingsChanged):
self._handle_settings(event)
elif isinstance(event, ResponseReceived):
if self.verbose:
print(f" Stream {event.stream_id}: Response received")
elif isinstance(event, DataReceived):
if self.verbose:
print(f" Stream {event.stream_id}: Data received ({len(event.data)} bytes)")
elif isinstance(event, StreamEnded):
if self.verbose:
print(f" Stream {event.stream_id}: Ended normally")
if event.stream_id in self.active_streams:
self.active_streams[event.stream_id]['ended'] = True
elif isinstance(event, StreamReset):
if self.verbose:
print(f" Stream {event.stream_id}: Reset (error code: {event.error_code})")
if event.stream_id in self.active_streams:
self.active_streams[event.stream_id]['reset'] = True
# Send any pending data
data_to_send = self.h2_conn.data_to_send()
if data_to_send:
self.socket.sendall(data_to_send)
except socket.timeout:
pass
except Exception as e:
if self.verbose:
print(f"Error receiving data: {e}")
return events
def _handle_settings(self, event: RemoteSettingsChanged):
"""Handle server settings"""
for setting, value in event.changed_settings.items():
setting_name = setting.name if hasattr(setting, 'name') else str(setting)
if self.verbose:
print(f" Server setting: {setting_name} = {value}")
# Check for MAX_CONCURRENT_STREAMS
if 'MAX_CONCURRENT_STREAMS' in setting_name:
self.server_max_streams = value
if self.verbose:
print(f"Server advertises max concurrent streams: {value}")
def send_stream_request(self, stream_id: int) -> bool:
"""Send a GET request on a specific stream"""
try:
headers = [
(':method', 'GET'),
(':path', self.path),
(':scheme', 'https' if self.use_tls else 'http'),
(':authority', self.host),
('user-agent', 'HTTP2-Stream-Limit-Tester/1.0'),
]
self.h2_conn.send_headers(stream_id, headers, end_stream=True)
data_to_send = self.h2_conn.data_to_send()
if data_to_send:
self.socket.sendall(data_to_send)
self.active_streams[stream_id] = {
'sent': time.time(),
'ended': False,
'reset': False
}
return True
except ProtocolError as e:
if self.verbose:
print(f" Stream {stream_id}: Protocol error - {e}")
return False
except Exception as e:
if self.verbose:
print(f" Stream {stream_id}: Failed to send - {e}")
return False
def test_concurrent_streams(
self,
max_streams_to_test: int = 200,
batch_size: int = 10,
delay_between_batches: float = 0.1
) -> StreamLimitTestResults:
"""
Test maximum concurrent streams by opening multiple streams
Args:
max_streams_to_test: Maximum number of streams to attempt
batch_size: Number of streams to open per batch
delay_between_batches: Delay in seconds between batches
"""
results = StreamLimitTestResults()
start_time = time.time()
print(f"\nTesting HTTP/2 Stream Limits:")
print(f" Target: {self.host}:{self.port}")
print(f" Max streams to test: {max_streams_to_test}")
print(f" Batch size: {batch_size}")
print("=" * 60)
try:
# Connect and get initial settings
if not self.connect():
results.errors.append("Failed to establish connection")
return results
results.advertised_max_streams = self.server_max_streams
if self.server_max_streams:
print(f"\nServer advertised limit: {self.server_max_streams} concurrent streams")
else:
print(f"\nServer did not advertise MAX_CONCURRENT_STREAMS limit")
# Start opening streams in batches
stream_id = 1 # HTTP/2 client streams use odd numbers
streams_opened = 0
while streams_opened results.reset_streams:
new_resets = reset_count - results.reset_streams
results.reset_streams = reset_count
print(f" WARNING: {new_resets} stream(s) were reset by server")
# If we're getting lots of resets, enforcement is happening
if reset_count > (results.successful_streams * 0.1):
results.enforcement_detected = True
print(f" Stream limit enforcement detected")
# Small delay between batches
if delay_between_batches > 0 and streams_opened 128:
print(f"\nWARNING: Advertised limit ({results.advertised_max_streams}) exceeds recommended maximum (128)")
print(" Consider reducing http2_max_concurrent_streams")
elif results.advertised_max_streams and results.advertised_max_streams 150:
print(f"\nWARNING: Opened {results.actual_max_streams} streams without enforcement")
print(" Server may be vulnerable to stream exhaustion attacks")
elif results.enforcement_detected:
print(f"\nServer actively enforces stream limits")
print(" Stream limit protection is working correctly")
if results.errors:
print(f"\nErrors encountered:")
for error in results.errors:
print(f" {error}")
print("=" * 60 + "\n")
def close(self):
"""Close the connection"""
try:
if self.h2_conn:
self.h2_conn.close_connection()
if self.socket:
data_to_send = self.h2_conn.data_to_send()
if data_to_send:
self.socket.sendall(data_to_send)
if self.socket:
self.socket.close()
if self.verbose:
print("Connection closed")
except Exception as e:
if self.verbose:
print(f"Error closing connection: {e}")
def main():
parser = argparse.ArgumentParser(
description='Test HTTP/2 server maximum concurrent streams',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic test
python3 http2_stream_limit_tester.py --host example.com
# Test with custom parameters
python3 http2_stream_limit_tester.py --host example.com --max-streams 300 --batch 20
# Verbose output
python3 http2_stream_limit_tester.py --host example.com --verbose
# Test specific path
python3 http2_stream_limit_tester.py --host example.com --path /api/health
# Test non-TLS HTTP/2 (h2c)
python3 http2_stream_limit_tester.py --host localhost --port 8080 --no-tls
Prerequisites:
pip3 install h2 hyper --break-system-packages
"""
)
parser.add_argument('--host', required=True, help='Target hostname')
parser.add_argument('--port', type=int, default=443, help='Target port (default: 443)')
parser.add_argument('--path', default='/', help='Request path (default: /)')
parser.add_argument('--no-tls', action='store_true', help='Disable TLS (for h2c testing)')
parser.add_argument('--max-streams', type=int, default=200,
help='Maximum streams to test (default: 200)')
parser.add_argument('--batch', type=int, default=10,
help='Streams per batch (default: 10)')
parser.add_argument('--delay', type=float, default=0.1,
help='Delay between batches in seconds (default: 0.1)')
parser.add_argument('--timeout', type=int, default=30,
help='Connection timeout in seconds (default: 30)')
parser.add_argument('--verbose', action='store_true', help='Enable verbose output')
args = parser.parse_args()
print("=" * 60)
print("HTTP/2 Maximum Concurrent Streams Tester")
print("=" * 60)
tester = HTTP2StreamLimitTester(
host=args.host,
port=args.port,
path=args.path,
use_tls=not args.no_tls,
timeout=args.timeout,
verbose=args.verbose
)
try:
results = tester.test_concurrent_streams(
max_streams_to_test=args.max_streams,
batch_size=args.batch,
delay_between_batches=args.delay
)
tester.display_results(results)
except KeyboardInterrupt:
print("\n\nTest interrupted by user")
except Exception as e:
print(f"\nFatal error: {e}")
if args.verbose:
import traceback
traceback.print_exc()
if __name__ == '__main__':
main() 5. Using the Script
5.1. Basic Usage
Test your server with default settings:
python3 http2_stream_limit_tester.py --host example.com 5.2. Advanced Examples
Test with increased stream count:
python3 http2_stream_limit_tester.py --host example.com --max-streams 300 --batch 20 Verbose output for debugging:
python3 http2_stream_limit_tester.py --host example.com --verbose Test specific API endpoint:
python3 http2_stream_limit_tester.py --host api.example.com --path /v1/health Test non-TLS HTTP/2 (h2c):
python3 http2_stream_limit_tester.py --host localhost --port 8080 --no-tls Gradual escalation test:
# Start conservative
python3 http2_stream_limit_tester.py --host example.com --max-streams 50
# Increase if server handles well
python3 http2_stream_limit_tester.py --host example.com --max-streams 100
# Push to limits
python3 http2_stream_limit_tester.py --host example.com --max-streams 200 Fast burst test:
python3 http2_stream_limit_tester.py --host example.com --max-streams 150 --batch 30 --delay 0.01 Slow ramp test:
python3 http2_stream_limit_tester.py --host example.com --max-streams 200 --batch 5 --delay 0.5 6. Understanding the Results
The script provides detailed output including:
- Advertised max streams: What the server claims to support
- Successful stream opens: How many streams were successfully created
- Failed stream opens: Streams that failed to open
- Streams reset by server: Streams terminated by the server (enforcement)
- Actual max achieved: The real concurrent stream limit
6.1. Example Output
Testing HTTP/2 Stream Limits:
Target: example.com:443
Max streams to test: 200
Batch size: 10
============================================================
Server advertised limit: 128 concurrent streams
Opening batch of 10 streams (total: 10)...
Opening batch of 10 streams (total: 20)...
Opening batch of 10 streams (total: 130)...
WARNING: 5 stream(s) were reset by server
Stream limit enforcement detected
============================================================
STREAM LIMIT TEST RESULTS
============================================================
Server Configuration:
Advertised max streams: 128
Test Statistics:
Successful stream opens: 130
Failed stream opens: 0
Streams reset by server: 5
Actual max achieved: 125
Test duration: 3.45s
Enforcement:
Stream limit enforcement: DETECTED
============================================================
ASSESSMENT
============================================================
Advertised limit (128) is within recommended range
Server actively enforces stream limits
Stream limit protection is working correctly
============================================================ 7. Interpreting Different Scenarios
7.1. Scenario 1: Proper Enforcement
Advertised max streams: 100
Successful stream opens: 105
Streams reset by server: 5
Actual max achieved: 100
Stream limit enforcement: DETECTED Analysis: Server properly enforces the limit. Configuration is working exactly as expected.
7.2. Scenario 2: No Enforcement
Advertised max streams: 128
Successful stream opens: 200
Streams reset by server: 0
Actual max achieved: 200
Stream limit enforcement: NOT DETECTED Analysis: Server accepts far more streams than advertised. This is a potential vulnerability that should be investigated.
7.3. Scenario 3: No Advertised Limit
Advertised max streams: Not specified
Successful stream opens: 200
Streams reset by server: 0
Actual max achieved: 200
Stream limit enforcement: NOT DETECTED Analysis: Server does not advertise or enforce limits. High risk configuration that requires immediate remediation.
7.4. Scenario 4: Conservative Limit
Advertised max streams: 50
Successful stream opens: 55
Streams reset by server: 5
Actual max achieved: 50
Stream limit enforcement: DETECTED Analysis: Very conservative limit. Good for security but may impact performance for legitimate high-throughput applications.
8. Monitoring During Testing
8.1. Server Side Monitoring
While running tests, monitor your server for resource utilization and connection metrics.
Monitor connection states:
netstat -an | grep :443 | awk '{print $6}' | sort | uniq -c Count active connections:
netstat -an | grep ESTABLISHED | wc -l Count SYN_RECV connections:
netstat -an | grep SYN_RECV | wc -l Monitor system resources:
top -l 1 | head -10 8.2. Web Server Specific Monitoring
For Nginx, watch active connections:
watch -n 1 'curl -s https://localhost/nginx_status | grep Active' For Apache, monitor server status:
watch -n 1 'curl -s https://localhost/server-status | grep requests' Check HTTP/2 connections:
netstat -an | grep :443 | grep ESTABLISHED | wc -l Monitor stream counts (if your server exposes this metric):
curl -s https://localhost:9090/metrics | grep http2_streams Monitor CPU and memory:
top -l 1 | grep -E "CPU|PhysMem" Check file descriptors:
lsof -i :443 | wc -l 8.3. Using tcpdump
Monitor packets in real time:
# Watch SYN packets
sudo tcpdump -i en0 'tcp[tcpflags] & tcp-syn != 0' -n
# Watch RST packets
sudo tcpdump -i en0 'tcp[tcpflags] & tcp-rst != 0' -n
# Watch specific host and port
sudo tcpdump -i en0 host example.com and port 443 -n
# Save to file for later analysis
sudo tcpdump -i en0 -w test_capture.pcap host example.com 8.4. Using Wireshark
For detailed packet analysis:
# Install Wireshark
brew install --cask wireshark
# Run Wireshark
sudo wireshark
# Or use tshark for command line
tshark -i en0 -f "host example.com" 9. Remediation Steps
If your tests reveal issues, apply these configuration fixes:
9.1. Nginx Configuration
http {
# Set conservative concurrent stream limit
http2_max_concurrent_streams 100;
# Additional protections
http2_recv_timeout 10s;
http2_idle_timeout 30s;
http2_max_field_size 16k;
http2_max_header_size 32k;
} 9.2. Apache Configuration
Set in httpd.conf or virtual host configuration:
# Set maximum concurrent streams
H2MaxSessionStreams 100
# Additional HTTP/2 settings
H2StreamTimeout 10
H2MinWorkers 10
H2MaxWorkers 150
H2StreamMaxMemSize 65536 9.3. HAProxy Configuration
defaults
timeout http-request 10s
timeout http-keep-alive 10s
frontend fe_main
bind :443 ssl crt /path/to/cert.pem alpn h2,http/1.1
# Limit streams per connection
http-request track-sc0 src table connection_limit
http-request deny if { sc_conn_cur(0) gt 100 } 9.4. Envoy Configuration
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http2_protocol_options:
max_concurrent_streams: 100
initial_stream_window_size: 65536
initial_connection_window_size: 1048576 9.5. Caddy Configuration
example.com {
encode gzip
# HTTP/2 settings
protocol {
experimental_http3
max_concurrent_streams 100
}
reverse_proxy localhost:8080
} 10. Combining with Rapid Reset Testing
You can use both the stream limit tester and the Rapid Reset tester together for comprehensive HTTP/2 security assessment:
# Step 1: Test stream limits
python3 http2_stream_limit_tester.py --host example.com
# Step 2: Test rapid reset with IP spoofing
sudo python3 http2rapidresettester_macos.py \
--host example.com \
--cidr 192.168.1.0/24 \
--packets 1000
# Step 3: Re-test stream limits to verify no degradation
python3 http2_stream_limit_tester.py --host example.com 11. Security Best Practices
11.1. Configuration Guidelines
- Set explicit limits: Never rely on default values
- Use conservative values: 100-128 streams is the recommended range
- Monitor enforcement: Regularly verify that limits are actually being enforced
- Document settings: Maintain records of your stream limit configuration
- Test after changes: Always test after configuration modifications
11.2. Defense in Depth
Stream limits should be one layer in a comprehensive security strategy:
- Stream limits: Prevent excessive concurrent streams per connection
- Connection limits: Limit total connections per IP address
- Request rate limiting: Throttle requests per second
- Resource quotas: Set memory and CPU limits
- WAF/DDoS protection: Use cloud-based or on-premise DDoS mitigation
11.3. Regular Testing Schedule
Establish a regular testing schedule:
- Weekly: Automated basic stream limit tests
- Monthly: Comprehensive security testing including Rapid Reset
- After changes: Always test after configuration or infrastructure changes
- Quarterly: Full security audit including penetration testing
12. Troubleshooting
12.1. Common Errors
Error: “SSL: CERTIFICATE_VERIFY_FAILED”
This occurs when testing against servers with self-signed certificates. For testing purposes only, you can modify the script to skip certificate verification (not recommended for production testing).
Error: “h2 library not installed”
Install the required library:
pip3 install h2 hyper --break-system-packages Error: “Connection refused”
Verify the port is open:
telnet example.com 443 Check if HTTP/2 is enabled:
curl -I --http2 https://example.com Error: “HTTP/2 not negotiated”
The server may not support HTTP/2. Verify with:
curl -I --http2 https://example.com | grep -i http/2 12.2. No Streams Being Reset
If streams are not being reset despite exceeding the advertised limit:
- Server may not be enforcing limits properly
- Configuration may not have been applied (restart required)
- Server may be using a different enforcement mechanism
- Limits may be set at a different layer (load balancer vs web server)
12.3. High Failure Rate
If many streams fail to open:
- Network connectivity issues
- Firewall blocking requests
- Server resource exhaustion
- Rate limiting triggering prematurely
13. Understanding the Attack Surface
When testing your infrastructure, consider all HTTP/2 endpoints:
- Web servers: Nginx, Apache, IIS
- Load balancers: HAProxy, Envoy, ALB
- API gateways: Kong, Tyk, AWS API Gateway
- CDN endpoints: CloudFlare, Fastly, Akamai
- Reverse proxies: Traefik, Caddy
13.1. Testing Strategy
Test at multiple layers:
# Test CDN edge
python3 http2_stream_limit_tester.py --host cdn.example.com
# Test load balancer directly
python3 http2_stream_limit_tester.py --host lb.example.com
# Test origin server
python3 http2_stream_limit_tester.py --host origin.example.com 14. Conclusion
Testing your HTTP/2 maximum concurrent streams configuration is essential for maintaining a secure and performant web infrastructure. This tool allows you to:
- Verify that your server advertises appropriate stream limits
- Confirm that advertised limits are actually enforced
- Identify misconfigurations before they can be exploited
- Tune performance while maintaining security
Regular testing, combined with proper configuration and monitoring, will help protect your infrastructure against HTTP/2-based attacks while maintaining optimal performance for legitimate users.
15. Additional Resources
- HTTP/2 RFC 7540
- SETTINGS Frame Documentation
- CVE-2023-44487 (Rapid Reset)
- Testing Your Website for HTTP/2 Rapid Reset Vulnerabilities
- Nginx HTTP/2 Module Documentation
- Apache mod_http2 Documentation
This guide and testing script are provided for educational and defensive security purposes only. Always obtain proper authorization before testing systems you do not own.