Guides
Guide

S3 Access Logs: Enable, Read, and Query With Athena

By Sazabi Team

Last verified on Nov 8, 2026.

S3 server access logging records requests made to a bucket and delivers them as log records, either to another S3 bucket as text files or to a CloudWatch Logs log group as JSON. It is off by default. This guide shows how to turn it on, what a record contains, how to query the files with Athena, and when CloudTrail is the better tool.

Server Access Logs vs CloudTrail

AWS recommends CloudTrail for bucket-level and object-level actions, and you can run both. The differences that matter in practice:

Server access logsCloudTrail
Delivery speedWithin a few hours, best effortData events every 5 minutes, management events every 15
FormatSpace-delimited text in a bucket, JSON in CloudWatch LogsJSON
Subset of objects by prefixNoYes
Failed authentication requestsRecordedNot delivered
Integrity validationNoYes
CostNo delivery charge to a bucket, you pay storageData events carry a fee on top of storage

By default, CloudTrail trails do not log data events such as GetObject, so object reads show up in CloudTrail only after you turn data events on. Server access logs are the only one of the two that capture requests that fail authentication.

Enabling Server Access Logging

You need a destination bucket in the same AWS Region and account as the source bucket. A dedicated logging bucket per Region is the simplest layout. Three constraints from AWS:

  • The destination must not have Object Lock or Requester Pays enabled.
  • It should not log to itself, since that creates an endless loop of logs about logs.
  • It must use SSE-S3 default encryption. With SSE-KMS, S3 might write objects encrypted with a key you cannot read.

In the console, open the source bucket, choose Properties, find Server access logging, and choose Edit. The console updates the destination bucket policy for you. From the CLI you must grant the logging service principal write access yourself:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3ServerAccessLogsPolicy",
      "Effect": "Allow",
      "Principal": { "Service": "logging.s3.amazonaws.com" },
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::amzn-s3-demo-destination-bucket/logs/*",
      "Condition": {
        "ArnLike": { "aws:SourceArn": "arn:aws:s3:::amzn-s3-demo-source-bucket" },
        "StringEquals": { "aws:SourceAccount": "111122223333" }
      }
    }
  ]
}
aws s3api put-bucket-policy --bucket amzn-s3-demo-destination-bucket --policy file://policy.json

cat > logging.json <<'JSON'
{
  "LoggingEnabled": {
    "TargetBucket": "amzn-s3-demo-destination-bucket",
    "TargetPrefix": "logs/"
  }
}
JSON

aws s3api put-bucket-logging --bucket amzn-s3-demo-source-bucket \
  --bucket-logging-status file://logging.json

Log objects are named TargetPrefix plus a timestamp and a unique string, for example logs/2026-11-08-04-11-52-UniqueString. You can choose date-based partitioning instead, which writes account/region/source-bucket/YYYY/MM/DD/ folders and pairs with a partitioned Athena table. Changes take effect over time: for about an hour after you enable logging or change the destination, some requests are logged and others are not.

Delivering to CloudWatch Logs Instead

You can also send the same records to a CloudWatch Logs log group in the bucket's Region, using vended log delivery. You configure it with CloudWatch Logs APIs, not S3 APIs, and you need logs:PutDeliverySource, logs:PutDeliveryDestination, logs:CreateDelivery, and s3:AllowVendedLogDeliveryForResource on the bucket.

aws logs put-delivery-source --name my-sal-source \
  --resource-arn arn:aws:s3:::my-bucket --log-type S3_SERVER_ACCESS_LOGS

aws logs put-delivery-destination --name my-sal-destination \
  --delivery-destination-configuration '{"destinationResourceArn": "arn:aws:logs:us-east-1:123456789012:log-group:my-sal-logs"}'

aws logs create-delivery --delivery-source-name my-sal-source \
  --delivery-destination-arn arn:aws:logs:us-east-1:123456789012:delivery-destination:my-sal-destination

Propagation takes 15 to 20 minutes, delivery then begins within a few hours, and nothing is backfilled. Each record is a JSON object, so CloudWatch Logs Insights can query it directly. CloudWatch vended log ingestion rates apply here, where bucket delivery has no delivery charge. See CloudWatch pricing.

filter http_status >= 400
| stats count(*) as errors by error_code, operation
| sort errors desc

Reading a Record

Bucket delivery produces one space-delimited line per request. The first fields are bucket owner, bucket, time in brackets (UTC), remote IP, requester, request ID, operation, key, the quoted request URI, HTTP status, and error code. Then come bytes sent, object size, total time and turn-around time in milliseconds, referer, user agent, version ID, host ID, signature version, cipher suite, authentication type, host header, and TLS version. A dash means the value is unknown or not applicable.

79a59df9...ef2be amzn-s3-demo-bucket1 [06/Feb/2019:00:00:38 +0000] 192.0.2.3 79a59df9...ef2be 3E57427F3EXAMPLE REST.GET.VERSIONING - "GET /amzn-s3-demo-bucket1?versioning HTTP/1.1" 200 - 113 - 7 - "-" "S3Console/0.4" -

AWS says it may add new fields to the end of each line, so write parsers that tolerate trailing fields they do not know. Records are best effort: one can arrive late, twice, or not at all, so do not use these logs as a complete accounting of requests.

Querying S3 Access Logs With Athena

Athena reads the files in place. Create a database in the same Region as the bucket, then a table over the log prefix. This is AWS's non-partitioned definition, and LOCATION points at your logging prefix:

CREATE EXTERNAL TABLE `s3_access_logs_db.mybucket_logs`(
  `bucketowner` STRING, `bucket_name` STRING, `requestdatetime` STRING,
  `remoteip` STRING, `requester` STRING, `requestid` STRING, `operation` STRING,
  `key` STRING, `request_uri` STRING, `httpstatus` STRING, `errorcode` STRING,
  `bytessent` BIGINT, `objectsize` BIGINT, `totaltime` STRING, `turnaroundtime` STRING,
  `referrer` STRING, `useragent` STRING, `versionid` STRING, `hostid` STRING,
  `sigv` STRING, `ciphersuite` STRING, `authtype` STRING, `endpoint` STRING,
  `tlsversion` STRING, `accesspointarn` STRING, `aclrequired` STRING, `sourceregion` STRING)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
  'input.regex'='([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$')
STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://amzn-s3-demo-bucket1-logs/logs/'

Then use queries such as these:

-- Who deleted an object, and when
SELECT requestdatetime, remoteip, requester, key
FROM s3_access_logs_db.mybucket_logs
WHERE key = 'images/picture.jpg' AND operation LIKE '%DELETE%';

-- Everything one IAM user did
SELECT * FROM s3_access_logs_db.mybucket_logs
WHERE requester = 'arn:aws:iam::123456789123:user/user_name';

-- Bytes sent to one address in June 2022
SELECT coalesce(SUM(bytessent), 0) AS bytessenttotal
FROM s3_access_logs_db.mybucket_logs
WHERE remoteip = '192.0.2.1'
  AND parse_datetime(requestdatetime, 'dd/MMM/yyyy:HH:mm:ss Z')
  BETWEEN parse_datetime('2022-06-01', 'yyyy-MM-dd') AND parse_datetime('2022-07-01', 'yyyy-MM-dd');

For 5xx errors, filter httpstatus LIKE '5%' and keep requestid and hostid so you can trace each failing request. The date-partitioned table definition uses a timestamp partition column, so a WHERE timestamp BETWEEN '2024/01/29' AND '2024/01/30' clause limits the scan to those days.

Searching S3 Activity With Sazabi

Sazabi, an AI-native observability and security platform, reads AWS logs through CloudWatch Logs subscription filters. If you deliver server access logs to a CloudWatch Logs log group, add that group as a stream under Settings > Log streams after connecting the CloudWatch source. For object-level activity, turn on CloudTrail data events, send the trail to CloudWatch Logs, and connect the CloudTrail source. Sazabi's docs say reading trails straight from their S3 bucket is not available yet, so use the CloudWatch Logs route for both. See the CloudTrail integration and the CloudWatch integration.

Frequently Asked Questions

How do I enable S3 access logs?

Open the bucket in the S3 console, choose Properties, then Server access logging, and pick a destination bucket in the same Region and account. From the CLI, grant the logging.s3.amazonaws.com principal s3:PutObject on the destination and run aws s3api put-bucket-logging.

How long do S3 access logs take to appear?

AWS delivers most records within a few hours, on a best-effort basis. Completeness and timeliness are not guaranteed, and a record can be late, duplicated, or missing.

Should I use S3 server access logs or CloudTrail?

AWS recommends CloudTrail data events for bucket and object actions because they are easier to set up and carry more detail. Server access logs are still useful for authentication failures, lifecycle actions, and low-cost traffic analysis in your own bucket.

Why are my S3 access logs not being delivered?

Check that the destination bucket is in the same Region and account, that the logging service principal can write to it, that it uses SSE-S3 rather than SSE-KMS, and that it does not have Object Lock or Requester Pays enabled. Then wait, because changes take effect over about an hour.

Sources

  1. Server access logging delivers to a CloudWatch Logs log group or an S3 general purpose bucket, best-effort delivery within a few hours, and settings changes taking effect over time (accessed Nov 8, 2026)
  2. Logging is off by default, same-Region and same-account destination bucket, the logging.s3.amazonaws.com principal, SSE-S3 requirement, put-bucket-logging command, and delivered key formats (accessed Nov 8, 2026)
  3. Delivering server access logs to CloudWatch Logs with put-delivery-source, put-delivery-destination, and create-delivery, required permissions, and the 15 to 20 minute propagation (accessed Nov 8, 2026)
  4. JSON field names for server access logs delivered to CloudWatch Logs (accessed Nov 8, 2026)
  5. Space-delimited record fields, the dash for unknown values, and trailing fields that may be added (accessed Nov 8, 2026)
  6. Athena table definition and example queries for deletes, requester activity, bytes sent, and 5xx errors; AWS recommends CloudTrail data events for identifying requests (accessed Nov 8, 2026)
  7. Comparison of CloudTrail and server access logs: delivery speed, format, prefix filtering, authentication failures, price (accessed Nov 8, 2026)
  8. Trails do not log data events by default and data events carry additional charges (accessed Nov 8, 2026)
  9. Sazabi reads CloudTrail through the trail's CloudWatch Logs delivery and does not read trails from S3 yet (accessed Nov 8, 2026)