This check flags API Gateway stages running without access logging, which means no record of who called your API, when, or with what result. Enable access logging on each stage by attaching a CloudWatch Logs log group and an access log format to the stage settings.
An API Gateway stage with no access logging is a blind spot. Requests flow through, get routed to backends, and return responses, but you have no durable record of any of it. When something goes wrong, whether it's an abuse pattern, a credential leak, or a customer reporting failed calls, you are left guessing because the evidence was never captured.
The apigw_accesslogging check in Lensix looks at every API Gateway stage in your AWS account and reports any that have access logging turned off. This post explains what the check covers, why it matters more than people expect, and how to fix it across the console, the CLI, and infrastructure as code.
What this check detects
API Gateway supports two distinct types of logging, and they are easy to confuse:
- Execution logging records what happens inside the gateway as it processes a request: integration calls, mapping templates, authorizer results. It is useful for debugging.
- Access logging records a single structured line per request describing the caller, the requested resource, the response status, latency, and other request metadata. This is the audit trail.
This check specifically targets access logging. A stage passes when it has an accessLogSettings configuration pointing at a CloudWatch Logs log group with a defined log format. It fails when that configuration is missing.
Note: Both REST APIs (API Gateway v1) and HTTP APIs (API Gateway v2) support access logging, but they configure it slightly differently. The CLI commands differ between apigateway and apigatewayv2. We cover both below.
Why it matters
Access logs are the difference between knowing what happened and reconstructing a guess after the fact. Here is where the gap bites in practice.
You cannot investigate what you did not log
Say a partner reports that a batch of API calls returned 403 errors last Tuesday. Without access logs, you have no per-request record of the source IP, the API key used, the resource path, or the exact response code. You are reduced to reproducing the issue or trawling backend logs that may not line up with what the gateway actually returned.
Abuse and credential theft go unnoticed
If an API key or IAM credential leaks, an attacker can hit your API at scale. Access logs let you spot anomalous source IPs, sudden spikes in 4xx responses, or calls to endpoints a given consumer never touches. Without them, the first sign of trouble is often a bill or a breached dataset, not a log line.
Warning: Backend logs are not a substitute for gateway access logs. Requests rejected by API Gateway itself, such as those blocked by a WAF rule, throttled, or failing an authorizer, may never reach your backend at all, so they leave no trace there.
Compliance frameworks expect it
SOC 2, PCI DSS, HIPAA, and most internal audit programs require an auditable record of access to systems handling sensitive data. An API fronting customer or payment data with no access logging is a finding waiting to happen. Auditors ask for who accessed what and when, and "we don't log that" is not an answer that passes.
Latency and error trends disappear
Access log fields like responseLatency and status feed dashboards and alarms that tell you when an API is degrading. Without them you lose a cheap, high-fidelity signal about the health of every endpoint.
How to fix it
Fixing this is a two-step job: create a CloudWatch Logs log group to receive the logs, then point your stage at it with a log format. Below are the steps for the console, CLI, and IaC.
Console (REST API)
- Open the API Gateway console and select your API.
- Choose Stages in the left navigation, then pick the stage.
- Open the Logs and tracing tab and edit the settings.
- Enable Custom access logging.
- Enter the ARN of a CloudWatch Logs log group, for example
arn:aws:logs:us-east-1:111122223333:log-group:/apigw/my-api. - Provide a log format. The console offers a JSON template you can use as a starting point.
- Save the changes.
CLI (REST API / apigateway)
First create a log group if you do not already have one:
aws logs create-log-group \
--log-group-name /apigw/my-api \
--region us-east-1
Then enable access logging on the stage. The patch operations below set the destination ARN and a JSON access log format:
aws apigateway update-stage \
--rest-api-id abc123def4 \
--stage-name prod \
--patch-operations \
op=replace,path=/accessLogSettings/destinationArn,value="arn:aws:logs:us-east-1:111122223333:log-group:/apigw/my-api" \
op=replace,path=/accessLogSettings/format,value='{ "requestId":"$context.requestId", "ip":"$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod", "resourcePath":"$context.resourcePath", "status":"$context.status", "protocol":"$context.protocol", "responseLength":"$context.responseLength", "responseLatency":"$context.responseLatency" }' \
--region us-east-1
CLI (HTTP API / apigatewayv2)
aws apigatewayv2 update-stage \
--api-id a1b2c3d4 \
--stage-name prod \
--access-log-settings DestinationArn="arn:aws:logs:us-east-1:111122223333:log-group:/apigw/my-http-api",Format='{ "requestId":"$context.requestId", "ip":"$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod", "routeKey":"$context.routeKey", "status":"$context.status", "protocol":"$context.protocol", "responseLength":"$context.responseLength", "responseLatency":"$context.responseLatency" }' \
--region us-east-1
Warning: CloudWatch Logs ingestion and storage are billed per GB. High-traffic APIs can generate large log volumes. Set a retention policy on the log group and trim the log format to the fields you actually use to keep costs predictable.
Set a sensible retention period so logs do not accumulate forever:
aws logs put-retention-policy \
--log-group-name /apigw/my-api \
--retention-in-days 90 \
--region us-east-1
Terraform
For a REST API stage, the access_log_settings block does the work:
resource "aws_cloudwatch_log_group" "apigw" {
name = "/apigw/my-api"
retention_in_days = 90
}
resource "aws_api_gateway_stage" "prod" {
rest_api_id = aws_api_gateway_rest_api.this.id
deployment_id = aws_api_gateway_deployment.this.id
stage_name = "prod"
access_log_settings {
destination_arn = aws_cloudwatch_log_group.apigw.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
status = "$context.status"
protocol = "$context.protocol"
responseLength = "$context.responseLength"
responseLatency = "$context.responseLatency"
})
}
}
For an HTTP API, use aws_apigatewayv2_stage with the same access_log_settings block and a route-aware format that includes $context.routeKey.
Tip: Define the log group and stage in the same module so the log group ARN flows directly into the stage. That removes the chance of a stage referencing a log group that was deleted or never created.
How to prevent it from happening again
One-off fixes drift. The goal is to make it impossible to ship a stage without access logging, or at least impossible without someone noticing.
Catch it in CI with policy as code
If you use Terraform, scan plans before they apply. A Checkov policy catches missing access logging on API Gateway stages out of the box:
checkov -d . --check CKV_AWS_76,CKV_AWS_95
For OPA / Conftest, write a Rego rule that rejects any stage resource lacking an access_log_settings block, and wire it into your pull request pipeline so the build fails before merge.
Tip: Wrap your API Gateway stage in an internal Terraform module that always includes a logging-enabled access_log_settings block. Teams consume the module and get logging for free, with no way to forget it.
Detect drift at runtime
Use AWS Config with the managed rule api-gw-execution-logging-enabled and a custom rule for access logging, or rely on Lensix to continuously evaluate stages and surface any that regress. CI gates stop new violations; continuous scanning catches the ones introduced by console edits or out-of-band changes.
Account-wide CloudWatch role
REST API access logging needs an IAM role with permission to write to CloudWatch Logs, set once per account in API Gateway settings. Configure it ahead of time so enabling logging on a stage never fails for a missing role:
aws apigateway update-account \
--patch-operations op=replace,path=/cloudwatchRoleArn,value="arn:aws:iam::111122223333:role/ApiGatewayCloudWatchLogsRole"
Best practices
- Log every stage, not just production. Dev and staging stages are often where credential leaks and misuse first appear. They are cheap to log given lower traffic.
- Use JSON log formats. Structured logs query cleanly in CloudWatch Logs Insights and ingest easily into SIEMs. Avoid free-form text formats.
- Include identity context. Capture
$context.identity.sourceIp,$context.identity.caller, and authorizer claims so logs answer "who" as well as "what". - Set retention deliberately. Match retention to your compliance requirements, then export to S3 with a lifecycle policy for cheaper long-term storage if you need to keep logs longer.
- Build alarms on log fields. Create metric filters for spikes in 4xx and 5xx responses or elevated latency so the logs do more than sit there.
- Pair access logging with WAF. Together they give you both the request record and the enforcement layer to act on what the logs reveal.
Access logging is one of the cheapest controls you can add to an API. The cost is a CloudWatch log group and a few lines of config. The payoff is an audit trail you will be very glad to have the day something goes sideways.
Run the apigw_accesslogging check across your account, fix the stages it flags, then lock the fix in place with a module and a CI gate so it never comes back.

