Guide · AWS Networking · Network Load Balancer
AWS Network Load Balancer for MCP Server Monitoring
ALB is the default choice for most HTTP workloads, but MCP servers often need NLB: WebSocket and SSE connections stay alive through NLB's 350-second TCP idle timeout (vs ALB's 60-second HTTP idle timeout), NLB assigns static Elastic IPs for firewall allowlisting, and TLS pass-through lets your MCP process own TLS negotiation end-to-end. AliveMCP runs its probe fleet behind an NLB and monitors NLB-fronted MCP endpoints daily — this guide covers why NLB fits MCP workloads, CDK setup for IP-target Fargate tasks, and the metrics that distinguish NLB health from the MCP application health underneath it. For health check configuration see NLB Health Checks for MCP Server Endpoints; for TLS termination see TLS at NLB for MCP Servers; for WebSocket/SSE connection management see WebSocket and SSE over NLB.
TL;DR
Use NLB when your MCP server uses WebSocket or SSE transport (long-lived connections), needs static IPs for client firewall rules, or handles >50,000 concurrent connections. Use ALB when you need HTTP routing rules, WAF integration, or per-path target group routing. The two aren't mutually exclusive — you can put an ALB behind an NLB for HTTP/2 with static IPs. For pure MCP-over-SSE workloads running on ECS Fargate, NLB with IP targets is the lower-friction path.
NLB vs ALB: what matters for MCP servers
The MCP specification currently defines two transports: stdio (local process communication) and HTTP with Server-Sent Events (SSE). Remote MCP deployments use SSE exclusively — a long-lived HTTP GET that the server streams events into. This connection lifetime changes which load balancer wins.
| Property | NLB | ALB | Why it matters for MCP |
|---|---|---|---|
| Layer | L4 (TCP/UDP/TLS) | L7 (HTTP/HTTPS) | NLB doesn't inspect HTTP headers; ALB must parse every request for routing rules |
| Idle timeout (TCP) | 350 seconds (fixed) | 60 seconds (configurable 1–4000s) | ALB's default 60s closes idle SSE connections; NLB's 350s TCP timeout rarely interrupts MCP streams |
| Static IPs | Yes — 1 Elastic IP per AZ | No — dynamic IPs from DNS | Enterprise MCP clients behind corporate firewalls can allowlist specific IPs; ALB DNS changes require policy updates |
| Client IP preservation | Native (source IP visible to targets) | Via X-Forwarded-For header | AliveMCP can log real client IPs without header parsing; useful for per-IP rate limiting in MCP servers |
| TLS pass-through | Yes (TCP listener) | No — ALB always terminates TLS | MCP servers with mTLS authentication need TLS pass-through; ALB would strip the client certificate |
| WebSocket | Transparent (TCP) | Supported with upgrade headers | NLB passes WebSocket bytes without inspection; ALB requires HTTP upgrade handling but supports sticky sessions |
| Throughput | Millions of req/s per AZ | Scales but processes at L7 | At very high probe rates (AliveMCP monitoring 100K+ endpoints), NLB's L4 throughput prevents load balancer becoming bottleneck |
| WAF integration | No | Yes (AWS WAF) | If you need per-request WAF rules, ALB is required; NLB has no application-layer inspection |
| Pricing (per LCU) | $0.006/LCU-hr | $0.008/LCU-hr | NLB is 25% cheaper per load balancer capacity unit at equal connection counts |
For most MCP servers that use SSE transport and run behind a single ECS service, NLB wins on idle timeout and static IPs. If you need host-based routing across multiple MCP endpoints on the same domain, ALB is necessary — NLB doesn't route by hostname or path.
CDK: NLB with IP targets for ECS Fargate
ECS Fargate tasks don't have stable instance IDs — they have private IPs. NLB supports two target types: ip (register by IP, required for Fargate) and instance (register by EC2 instance ID, only for EC2 launch type). The ip target type registers each Fargate task IP directly, avoiding the ENI attachment limitation of instance targets.
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { Duration, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export function createMcpNlb(
scope: Construct,
vpc: ec2.IVpc,
cluster: ecs.Cluster,
mcpService: ecs.FargateService,
): elbv2.NetworkLoadBalancer {
// Allocate one Elastic IP per AZ for static addressing
const eipA = new ec2.CfnEIP(scope, 'NlbEipA', { domain: 'vpc' });
const eipB = new ec2.CfnEIP(scope, 'NlbEipB', { domain: 'vpc' });
const nlb = new elbv2.NetworkLoadBalancer(scope, 'McpNlb', {
vpc,
internetFacing: true,
// Cross-zone load balancing: disable to preserve AZ affinity and avoid
// inter-AZ data transfer charges on NLB (unlike ALB, NLB charges for cross-AZ bytes)
crossZoneEnabled: false,
// Assign fixed Elastic IPs for firewall allowlisting
vpcSubnets: {
subnets: vpc.publicSubnets,
// subnet-to-EIP mapping via L1 override below
},
});
// Target group: IP type for Fargate, 8080 is MCP SSE port
const targetGroup = new elbv2.NetworkTargetGroup(scope, 'McpTargetGroup', {
vpc,
port: 8080,
protocol: elbv2.Protocol.TCP,
targetType: elbv2.TargetType.IP,
// Deregistration delay: how long NLB waits for in-flight SSE streams to complete
// before removing a draining task; 120s matches ECS container stop timeout
deregistrationDelay: Duration.seconds(120),
// Preserve client IP so MCP server receives real caller IP (not NLB IP)
preserveClientIp: true,
healthCheck: {
protocol: elbv2.Protocol.HTTP,
path: '/health',
port: '8080',
interval: Duration.seconds(10),
healthyThresholdCount: 2,
unhealthyThresholdCount: 2,
},
});
// TLS listener: NLB terminates TLS, forwards plain TCP to targets
nlb.addListener('McpTlsListener', {
port: 443,
protocol: elbv2.Protocol.TLS,
certificates: [
elbv2.ListenerCertificate.fromArn(
`arn:aws:acm:${Stack.of(scope).region}:${Stack.of(scope).account}:certificate/YOUR_CERT_ID`,
),
],
defaultTargetGroups: [targetGroup],
// TLS 1.2+ with forward secrecy; ECDHE ciphers required for mTLS MCP clients
sslPolicy: elbv2.SslPolicy.RECOMMENDED_TLS,
});
// Also expose plain TCP on port 80 for internal health checks
nlb.addListener('McpTcpListener', {
port: 80,
protocol: elbv2.Protocol.TCP,
defaultTargetGroups: [targetGroup],
});
// Attach ECS service to NLB target group
mcpService.attachToNetworkTargetGroup(targetGroup);
return nlb;
}
The preserveClientIp: true setting on the target group causes NLB to send packets with the original source IP. This means the MCP container's security group must allow inbound from the entire client IP range, not just the NLB's IPs. If you're restricting traffic to known AliveMCP probe IPs, this is the right layer to enforce it.
Cross-zone load balancing: default off on NLB
NLB has cross-zone disabled by default; ALB has it enabled by default. The difference matters for both cost and traffic distribution:
| Setting | Behavior | Cost | Recommendation for MCP |
|---|---|---|---|
crossZoneEnabled: false (default) |
Each NLB node routes only to targets in its own AZ; AZ with fewer healthy tasks gets less traffic proportionally | No inter-AZ NLB data transfer charges | Use when your ECS tasks are spread evenly across AZs (ECS does this by default with spread placement) |
crossZoneEnabled: true |
Each NLB node routes to all healthy targets across all AZs; equal distribution even when tasks are AZ-imbalanced | $0.01/GB for cross-AZ NLB data transfer | Use when tasks concentrate in one AZ during Spot interruptions or during deployment rollouts |
For AliveMCP's probe workers behind an NLB, cross-zone is off. The coordinator service (single task in one AZ) uses cross-zone on its internal NLB so all workers can reach it regardless of AZ. Worker-to-coordinator calls are low-volume (registry fetches, not probe results), so cross-AZ transfer costs are negligible.
NLB metrics for MCP server health monitoring
NLB publishes per-target-group and per-load-balancer metrics to CloudWatch. The metrics that signal MCP server problems are different from the metrics that signal NLB infrastructure problems:
| Metric | Namespace | What it signals |
|---|---|---|
HealthyHostCount |
AWS/NetworkELB | How many MCP server tasks NLB considers healthy. Drops to 0 = full outage. |
UnHealthyHostCount |
AWS/NetworkELB | Tasks failing health checks. Rises before HealthyHostCount drops (partially degraded state). |
ActiveFlowCount |
AWS/NetworkELB | Current open TCP connections (each SSE stream = 1 flow). Drop indicates client disconnects; spike indicates connection leak. |
NewFlowCount |
AWS/NetworkELB | New connections per second. Spike indicates reconnection storm (clients retrying after server restart). |
ProcessedBytes |
AWS/NetworkELB | Bytes passing through NLB. Drop suggests MCP servers are sending fewer events; spike suggests large probe result payloads. |
TCP_ELB_Reset_Count |
AWS/NetworkELB | RST packets sent by NLB. Spikes after NLB timeout (350s idle) or when target fails mid-connection. |
TargetTLSNegotiationErrorCount |
AWS/NetworkELB | TLS handshake failures between NLB and backend (TLS-to-target mode). Indicates cert mismatch or expired backend cert. |
AliveMCP creates CloudWatch alarms on HealthyHostCount < 1 (immediate page) and UnHealthyHostCount > 0 for 5 minutes (warning). The NLB health check failure is a leading indicator — AliveMCP's external probe will see failures 10–30 seconds after NLB marks a target unhealthy, but the NLB alarm fires first.
Security group rules for NLB with IP targets
NLB with preserveClientIp: true doesn't use the NLB's IP as the source IP when forwarding packets — it preserves the original client IP. This means your ECS task security group must allow inbound from 0.0.0.0/0 (or the known client CIDR) on the MCP port, not from the NLB security group.
// ECS task security group for NLB with preserved client IP
const mcpTaskSg = new ec2.SecurityGroup(scope, 'McpTaskSg', {
vpc,
description: 'MCP server Fargate task',
allowAllOutbound: true,
});
// NLB preserves source IP — allow from any client (or narrow to your probe CIDR)
mcpTaskSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(8080),
'NLB forward with client IP preservation',
);
// NLB health check probes originate from NLB node IPs in the VPC subnets
// Add the VPC CIDR to allow health check traffic
mcpTaskSg.addIngressRule(
ec2.Peer.ipv4(vpc.vpcCidrBlock),
ec2.Port.tcp(8080),
'NLB health checks from VPC CIDR',
);
If preserveClientIp is false (the default), the source IP seen by the target is the NLB node IP in the VPC subnet — you can restrict the task security group to the VPC CIDR only. For MCP servers that rate-limit by client IP, enabling client IP preservation is essential; for internal probe workers where the source IP is irrelevant, disabling it simplifies security group rules.
AliveMCP probe architecture behind NLB
AliveMCP's own probe infrastructure uses NLB in two places:
- Internal NLB for probe coordinator: Worker tasks need to fetch the current endpoint registry (batch assignments) from the coordinator. An internal NLB in front of the coordinator provides a stable DNS name (
coordinator.alivemcp.internal) that workers use regardless of which AZ the coordinator task is in. Because the coordinator is a single task, the NLB health check serves as a secondary restart trigger — if the coordinator container hangs (health check fails for 20 seconds), NLB marks it unhealthy and a CloudWatch alarm restarts the ECS service. - External NLB for the AliveMCP API + dashboard: The public SSE event stream (live uptime feed for registered MCP endpoints) is a long-lived SSE connection from browsers. NLB's 350-second TCP idle timeout handles the case where a browser receives no events (all endpoints are healthy) for several minutes without closing the stream. ALB's 60-second default would disconnect the browser every minute.
For monitored MCP endpoints (the third-party servers AliveMCP probes), the probe worker opens a new HTTP/HTTPS connection for each 60-second probe cycle and closes it after receiving the first valid SSE event or a timeout. These short-lived probe connections don't benefit from NLB vs ALB — what matters is whether the monitored MCP server is behind an NLB, which AliveMCP detects and reports in the endpoint's status page (TCP reset behavior, TLS version, connection establishment latency).