Your application runs in a private subnet and calls SQS, SNS, or Secrets Manager. The access works, but the bytes travel through a NAT Gateway and show up on the bill. One possible fix is a VPC Interface Endpoint, which keeps that traffic on the AWS network. The less automatic part is deciding whether the endpoint cuts costs or merely replaces one billing line with another.
That decision comes down to three inputs: which services receive the traffic, how many bytes flow to them, and how many Availability Zones need the endpoint. For S3 and DynamoDB, the answer is usually different: a Gateway Endpoint, with no additional hourly or data-processing charge.
By the end, you will have a reproducible calculation, a CDK example, and a production-safe way to confirm that the bytes have left the NAT path. Finding an expensive NAT at the end of the month is financial observability, just with rather too much latency.
Versions and prerequisites
The example uses aws-cdk-lib 2.268.0 and the aws-cdk CLI package 2.1140.0. The different version schemes are expected: constructs remain on the 2.x line, while the CLI has adopted its new numbering. Use AWS CLI v2 for the verification steps.
To validate the template locally, run:
npx aws-cdk@2.1140.0 synth
This synthesizes the stack; it is not a deployment. The pricing calculation uses us-east-1 (N. Virginia). NAT rates come from the AmazonEC2 CSV published at 2026-09-04T23:11:17Z, with SKUs effective since 2026-09-01. Interface Endpoint rates come from the AmazonVPC catalog with a publicationDate of 2026-08-31T09:22:32Z.
Gateway and Interface endpoints solve different problems
The names are similar, but the mechanisms and charges are not.
A Gateway Endpoint serves only S3 and DynamoDB. It adds a prefix-list route to the subnet route table. It creates no ENI, does not use PrivateLink, and has no additional hourly or data-processing charge. For an application inside the VPC accessing S3 or DynamoDB, this should be the first option.
An Interface Endpoint uses AWS PrivateLink and creates an ENI with a private IP address in every selected subnet. With private DNS enabled, the AWS service's public hostname resolves to those private addresses from inside the VPC. This is the mechanism for APIs without a Gateway Endpoint, including SQS, SNS, and Secrets Manager.
It is easy to confuse Gateway and Interface endpoints until the bill explains the distinction with admirable directness.
| Type | Services and common use | Mechanism | Endpoint charge |
|---|---|---|---|
| Gateway | S3 and DynamoDB from inside the VPC | Route in the route table; no ENI and no PrivateLink | No additional hourly or data-processing charge |
| Interface | SQS, SNS, Secrets Manager, and other supported services | ENI per subnet/AZ through PrivateLink | Per endpoint per AZ, plus data processed |
S3 and DynamoDB also offer Interface Endpoints, but that does not make the paid option the default. For S3, an Interface Endpoint makes sense when access must come from an on-premises environment, a peered VPC in another Region, or through Transit Gateway. When a Gateway Endpoint handles the route from inside the VPC, use the Gateway Endpoint.
The official documentation covers both Gateway Endpoints and creating Interface Endpoints.
Official pricing and the payback sketch
In us-east-1, a NAT Gateway costs US$ 0.045 per active hour and US$ 0.045 per GB processed. An Interface Endpoint costs US$ 0.01 per hour, per endpoint, per AZ. For data processed by PrivateLink, the first tier, up to 1 PB, costs US$ 0.01 per GB; the next 4 PB costs US$ 0.006 per GB, and volume above 5 PB costs US$ 0.004 per GB.
The references are the official VPC pricing and AWS PrivateLink pricing pages.
Assume a 720-hour month, three AZs, one service with an Interface Endpoint, and a NAT that remains active for genuine internet access. This is an arithmetic sketch, not a production measurement.
| Component | Monthly calculation under these assumptions |
|---|---|
| One NAT retained | US$ 0.045/h × 720 h = US$ 32.40 |
| One Interface service in three AZs | 3 × US$ 0.01/h × 720 h = US$ 21.60 |
| Difference per GB moved | US$ 0.045 − US$ 0.01 = US$ 0.035/GB |
| Service break-even point | US$ 21.60 ÷ US$ 0.035/GB ≈ 617 GB/month |
The NAT's US$ 32.40 hourly cost is not counted as savings because the NAT remains in place. The reduction comes only from removing bytes destined for the AWS service from that path. Under these assumptions, an SQS endpoint in three AZs must divert roughly 617 GB per month to offset its US$ 21.60 monthly cost.
The threshold changes when the AZ count, traffic volume, or need for the NAT changes. Every additional Interface service brings its own hourly charge per AZ, even while idle. Add SQS, SNS, and Secrets Manager separately; do not treat “PrivateLink in the VPC” as a single subscription.
CDK: SQS through Interface, S3 and DynamoDB through Gateway
The following excerpt uses aws-cdk-lib 2.268.0. The SQS endpoint gets a policy that accepts calls when aws:PrincipalAccount matches the stack's account. S3 and DynamoDB are then added as Gateway Endpoints.
vpc.addInterfaceEndpoint('SqsEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SQS,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
policy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
principals: [new iam.AnyPrincipal()],
actions: ['sqs:*'],
resources: ['*'],
conditions: {
StringEquals: { 'aws:PrincipalAccount': Stack.of(this).account },
},
}),
],
}),
});
vpc.addGatewayEndpoint('S3Gateway', {
service: ec2.GatewayVpcEndpointAwsService.S3,
});
vpc.addGatewayEndpoint('DdbGateway', {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
});
SNS and Secrets Manager are also candidates for Interface Endpoints, but there is little value in repeating the same construct three times before measuring three different traffic flows.
The default endpoint policy grants full access with Principal *, Action *, and Resource *. Convenient, like every permission that schedules least-privilege review for some future Friday. The VPC endpoint policy documentation makes two important limits clear: an endpoint policy does not replace IAM identity policies, and not every service supports endpoint policies.
For global condition keys that use system-generated identifiers, such as aws:PrincipalAccount and aws:SourceVpc, do not use wildcards; apply StringEquals with a concrete identifier. On Gateway Endpoints, Principal must be *; when you need to restrict the principal, use a condition with aws:PrincipalArn. aws:SourceVpce, meanwhile, generally belongs in the resource policy, such as an S3 bucket policy, and does not replace the endpoint policy.
For private DNS to work, the VPC must have DNS hostnames and DNS resolution enabled. The Interface Endpoint's security group must also allow inbound traffic on port 443 from the VPC or from the tasks making the calls.
How to prove the bytes left the NAT path
The concrete fix is to create an Interface Endpoint for the service carrying most of the traffic—SQS in this example—in the required subnets, with private DNS and a restrictive policy. Then observe the route before and after without capturing application packets in production.
Start by confirming the state, type, and service of the endpoints:
aws ec2 describe-vpc-endpoints \
--query 'VpcEndpoints[].{Id:VpcEndpointId,Type:VpcEndpointType,Service:ServiceName,State:State}' \
--output table
The expected state is available. That proves the resource is ready, not that the application is using it.
For production-safe observation, compare NAT Gateway bytes in CloudWatch with the AWS/PrivateLinkEndpoints namespace and the BytesProcessed metric. As a financial alternative, use Cost Explorer to compare NatGateway-Bytes with VpcEndpoint-Bytes over the same period.
From a task inside the VPC, resolve sqs.us-east-1.amazonaws.com. With private DNS enabled, the response should point to private RFC1918 addresses. If you need further confirmation, configure VPC Flow Logs on the NAT and endpoint ENIs and compare the actual interface-id field between them. The default record includes fields such as interface-id, srcaddr, dstaddr, packets, and bytes; do not filter on imaginary endpoint fields.
Together, these checks answer three separate questions: whether the endpoint is available, whether DNS sends the application to a private IP address, and whether the bytes moved from NAT to PrivateLink.
When not to adopt an Interface Endpoint
Do not create the endpoint by reflex. We would step back from adoption when:
- traffic to that service remains well below hundreds of GB per month, and one ENI in each of three AZs leaves the payback far below the approximate 617 GB threshold in this scenario;
- S3 or DynamoDB is the destination and a Gateway Endpoint covers access from inside the VPC;
- the endpoint would sit idle while continuing to accrue hourly charges;
- the design uses only one AZ to save money even though availability requires three; concentrating ENIs can also add cross-AZ data-transfer costs;
- the service includes endpoint costs in its own pricing model, in which case its service-specific pricing page must be part of the decision.
And do not remove the NAT just because you added an SQS VPC Endpoint. The endpoint fixes the route for that service; access to other internet destinations still needs an egress path that fits the architecture.
Next step
Open Cost Explorer or CloudWatch, isolate the bytes for one AWS API without a Gateway Endpoint, and rerun the calculation using the real number of AZs. If one service consistently carries volume above the break-even point and the VPC must keep its NAT for other destinations, adopt that service's Interface Endpoint, restrict its policy, and confirm the migration through byte metrics and DNS. If traffic stays below the threshold, keep the NAT and measure again; for S3 and DynamoDB inside the VPC, use a Gateway Endpoint.