A fintech client running MySQL 8.0 on Amazon RDS reached out after their AWS bill climbed by $4,200 in a single month — not from compute, not from storage, but from CloudWatch Logs ingestion fees driven by database audit logging. Their compliance team had enabled full CONNECTION and QUERY audit logging to satisfy SOC 2 requirements, generating roughly 180 GB of log data monthly across three RDS instances. At $0.50 per GB ingested into CloudWatch Logs, the math was brutal: over $1,000 per instance per month, before factoring in the $0.03/GB/month retention cost. Routing those same logs to Amazon S3 and querying them with Athena dropped their monthly audit logging bill from $4,200 to under $210 — a 95% reduction — without sacrificing a single audit record.
- CloudWatch Logs charges $0.50/GB ingested; S3 costs $0.023/GB stored — a 21x price difference on storage alone.
- RDS MySQL supports five audit log types via the MariaDB Audit Plugin:
CONNECTION,QUERY,TABLE,QUERY_DML, andQUERY_DDL. - Enable log export to S3 via the RDS console or
aws rds modify-db-instance --cloudwatch-logs-export-configuration— no engine restart required. - Partition your S3 prefix by date (
year=YYYY/month=MM/day=DD/) and use Athena to run SQL directly over the log files. - S3 Lifecycle rules can automatically tier logs to S3 Glacier after 30 days and expire them at 365 days, satisfying most compliance retention windows at near-zero cost.
- Aurora MySQL supports direct audit-to-S3 natively via the
ADVANCED AUDITINGfeature — no CloudWatch hop required.
Why MySQL RDS Audit Logs Are So Expensive in CloudWatch
Amazon CloudWatch Logs pricing has two components that compound against high-volume database workloads: ingestion at $0.50 per GB and storage at $0.03 per GB per month. On a busy OLTP MySQL instance processing 5,000 queries per second, audit logging at the QUERY level generates log lines continuously. A single SELECT with a 200-byte query string produces an audit record of roughly 400 bytes after metadata is added. At 5,000 QPS, that is 2 MB/s of raw audit data — 172 GB per day, or 5.2 TB per month. Even at the QUERY_DML level (DML statements only), a moderately busy transactional database easily produces 20–50 GB/month.
The CloudWatch Logs pricing model was designed for application logs — typically kilobytes per minute per service instance. Database audit logs operate at a fundamentally different volume profile. S3 standard storage at $0.023/GB/month is 21 times cheaper than CloudWatch storage before you even account for the ingestion cost. For a workload generating 100 GB of audit data per month, the comparison looks like this:
| Component | CloudWatch Logs | S3 + Athena |
|---|---|---|
| Ingestion (100 GB) | $50.00 | $0.00 |
| Storage (100 GB/month) | $3.00 | $2.30 |
| Query / analysis | $0.005/GB scanned (Insights) | $5.00/TB scanned (Athena) |
| Total monthly estimate | ~$53–$80 | ~$2.50–$5 |
At 1 TB/month of audit data — realistic for a large multi-tenant SaaS database — CloudWatch costs exceed $500/month on ingestion alone. S3 storage for the same volume costs $23. The ingestion fee is the primary driver of the cost gap, and it is entirely avoidable.
Understanding MySQL RDS Audit Log Types
Amazon RDS MySQL uses the MariaDB Audit Plugin (also known as SERVER_AUDIT) rather than MySQL Enterprise Audit. The plugin supports five event categories, each controlled by the server_audit_events parameter in your RDS parameter group:
- CONNECTION — Records every login, logout, and failed authentication attempt. Includes the username, host, database, and return code. Essential for access control auditing. Low volume.
- QUERY — Records every executed SQL statement, including the full query text, affected rows, and execution time. Highest volume category by a significant margin. Use only when required by compliance mandate.
- TABLE — Records the table(s) accessed per statement. Less verbose than
QUERYbut still captures the access pattern. Useful for data residency compliance. - QUERY_DML — Records only
INSERT,UPDATE,DELETE,REPLACE, andTRUNCATE. The right choice for most SOC 2 and PCI DSS implementations — captures data mutation without logging everySELECT. - QUERY_DDL — Records only schema changes:
CREATE,ALTER,DROP,RENAME. Combine withQUERY_DMLfor comprehensive compliance coverage at a fraction ofQUERYvolume.
For SOC 2 Type II and PCI DSS compliance, CONNECTION,QUERY_DML,QUERY_DDL covers all required event categories at approximately 5–10% of the log volume generated by enabling QUERY. Switching to this combination alone typically reduces audit log volume by 90% before you change where the logs land.
Enabling the Audit Plugin on RDS MySQL
Step 1: Create a Custom Parameter Group
You cannot modify the default parameter group. Create a custom group first:
aws rds create-db-parameter-group \
--db-parameter-group-name mysql80-audit \
--db-parameter-group-family mysql8.0 \
--description "MySQL 8.0 with audit logging enabled"Step 2: Configure the Audit Parameters
aws rds modify-db-parameter-group \
--db-parameter-group-name mysql80-audit \
--parameters \
"ParameterName=server_audit_logging,ParameterValue=ON,ApplyMethod=immediate" \
"ParameterName=server_audit_events,ParameterValue=CONNECTION\,QUERY_DML\,QUERY_DDL,ApplyMethod=immediate" \
"ParameterName=server_audit_query_log_limit,ParameterValue=1024,ApplyMethod=immediate"The server_audit_query_log_limit parameter truncates long query strings in the log at the specified byte limit, preventing single large batch queries from producing multi-megabyte log lines. A value of 1024 is appropriate for most compliance requirements.
Step 3: Attach the Parameter Group to Your Instance
aws rds modify-db-instance \
--db-instance-identifier your-mysql-instance \
--db-parameter-group-name mysql80-audit \
--apply-immediatelyApplying a new parameter group with --apply-immediately triggers a reboot for static parameters. The audit plugin parameters listed above are dynamic — they apply without a reboot — but verify this in the AWS console under the parameter group's "Apply Type" column before running the command on a production instance.
Routing Audit Logs to S3 Instead of CloudWatch
RDS writes audit logs to the instance's local filesystem under /rdsdbdata/log/audit/. By default, logs stay on the instance and are accessible via the RDS console or aws rds download-db-log-file-portion. To export them to CloudWatch or S3, you configure log export. As of 2024, RDS MySQL supports native export to CloudWatch Logs; for S3, you route through CloudWatch using a subscription filter to Kinesis Data Firehose, or use a purpose-built Lambda export — but there is a simpler path.
Option A: CloudWatch Subscription Filter to Kinesis Firehose (Recommended)
This is the most operationally reliable approach. Enable the minimal CloudWatch export, then immediately drain it to S3 via Firehose — CloudWatch charges ingestion only once, and Firehose delivery to S3 costs $0.029/GB.
# 1. Enable audit log export to CloudWatch Logs
aws rds modify-db-instance \
--db-instance-identifier your-mysql-instance \
--cloudwatch-logs-export-configuration '{"EnableLogTypes":["audit"]}' \
--apply-immediately
# 2. Create a Firehose delivery stream targeting your S3 bucket
aws firehose create-delivery-stream \
--delivery-stream-name rds-audit-to-s3 \
--s3-destination-configuration '{
"RoleARN": "arn:aws:iam::123456789012:role/firehose-s3-role",
"BucketARN": "arn:aws:s3:::your-audit-log-bucket",
"Prefix": "rds-audit/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/",
"ErrorOutputPrefix": "rds-audit-errors/",
"BufferingHints": {"SizeInMBs": 128, "IntervalInSeconds": 300},
"CompressionFormat": "GZIP"
}'
# 3. Create a CloudWatch Logs subscription filter
aws logs put-subscription-filter \
--log-group-name /aws/rds/instance/your-mysql-instance/audit \
--filter-name rds-audit-firehose \
--filter-pattern "" \
--destination-arn "arn:aws:firehose:us-east-1:123456789012:deliverystream/rds-audit-to-s3"Option B: RDS Export via Lambda (Lower Latency, More Control)
For workloads requiring near-real-time S3 delivery or needing to avoid any CloudWatch dependency, deploy a Lambda function that polls aws rds describe-db-log-files and downloads and uploads each rotated log file to S3.
import boto3, os
rds = boto3.client('rds')
s3 = boto3.client('s3')
INSTANCE = os.environ['DB_INSTANCE_ID']
BUCKET = os.environ['AUDIT_BUCKET']
def handler(event, context):
paginator = rds.get_paginator('describe_db_log_files')
for page in paginator.paginate(DBInstanceIdentifier=INSTANCE,
FilenameContains='audit'):
for log_file in page['DescribeDBLogFiles']:
fname = log_file['LogFileName']
# Only export rotated (non-current) files
if fname.endswith('.log') and not fname.endswith('audit.log'):
resp = rds.download_db_log_file_portion(
DBInstanceIdentifier=INSTANCE,
LogFileName=fname,
Marker='0'
)
s3_key = f"rds-audit/{INSTANCE}/{fname}"
s3.put_object(Bucket=BUCKET, Key=s3_key,
Body=resp['LogFileData'])S3 Bucket Configuration: IAM Policy and Lifecycle Rules
Bucket Policy for Firehose Write Access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FirehoseAuditWrite",
"Effect": "Allow",
"Principal": {"Service": "firehose.amazonaws.com"},
"Action": ["s3:PutObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:GetBucketLocation"],
"Resource": [
"arn:aws:s3:::your-audit-log-bucket",
"arn:aws:s3:::your-audit-log-bucket/*"
],
"Condition": {
"StringEquals": {"aws:SourceAccount": "123456789012"}
}
}
]
}S3 Lifecycle Rule for Cost-Optimized Retention
{
"Rules": [
{
"ID": "AuditLogRetention",
"Status": "Enabled",
"Filter": {"Prefix": "rds-audit/"},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER_IR"},
{"Days": 180, "StorageClass": "DEEP_ARCHIVE"}
],
"Expiration": {"Days": 365}
}
]
}Apply this lifecycle rule via the console or CLI. With this configuration, a 100 GB/month audit workload costs roughly $2.30/month for the first 30 days, $1.25/month in S3-IA, $0.40/month in Glacier Instant Retrieval, and $0.10/month in Deep Archive — compared to $3.00/month continuously in CloudWatch.
Enable S3 server-side encryption with aws:kms using a CMK, not AES256, if your compliance framework requires key rotation. Set BucketKeyEnabled: true on the bucket to reduce per-request KMS API call costs by up to 99%. Also enable S3 Block Public Access and MFA Delete on the bucket — audit logs are a high-value target for attackers attempting to cover tracks.
Querying Audit Logs with Amazon Athena
Once logs land in S3, Amazon Athena lets you run standard SQL against them at $5.00 per TB scanned. With GZIP compression and date partitioning, typical audit log queries scan 1–5 GB, costing fractions of a cent per investigation.
Create the Athena Table
CREATE EXTERNAL TABLE rds_mysql_audit (
timestamp STRING,
serverhost STRING,
username STRING,
host STRING,
connectionid BIGINT,
queryid BIGINT,
operation STRING,
database STRING,
object STRING,
retcode INT
)
PARTITIONED BY (year STRING, month STRING, day STRING)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
'input.regex' = '^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d+)\\s+(\\d+)\\s+(\\w+)\\s+(\\S+)\\s+(.+?)\\s+(\\d+)$'
)
STORED AS TEXTFILE
LOCATION 's3://your-audit-log-bucket/rds-audit/'
TBLPROPERTIES ('compressionType'='gzip');
-- Load partitions
MSCK REPAIR TABLE rds_mysql_audit;Example Forensic Queries
-- Find all failed login attempts in the last 7 days
SELECT timestamp, username, host, retcode, connectionid
FROM rds_mysql_audit
WHERE year = '2025' AND month = '02'
AND operation = 'CONNECT'
AND retcode != 0
ORDER BY timestamp DESC;
-- Identify which users ran DDL in the past month
SELECT username, operation, object, COUNT(*) AS cnt
FROM rds_mysql_audit
WHERE year = '2025'
AND operation IN ('CREATE_TABLE','ALTER_TABLE','DROP_TABLE','RENAME_TABLE')
GROUP BY username, operation, object
ORDER BY cnt DESC;
-- Audit all DML against a specific table
SELECT timestamp, username, host, operation, retcode
FROM rds_mysql_audit
WHERE year = '2025' AND month = '02'
AND object LIKE '%payments%'
AND operation IN ('WRITE','UPDATE','DELETE')
ORDER BY timestamp DESC
LIMIT 500;Always include partition predicates (year, month, day) in your WHERE clause. An unpartitioned full-table scan of a year of audit logs at 100 GB/month costs $6 in Athena. The same query scoped to a single day costs $0.02.
Log Rotation and Retention on the RDS Instance
The MariaDB Audit Plugin rotates audit log files based on size. The RDS-managed rotation parameter is server_audit_file_rotate_size, which defaults to 1,000,000 bytes (1 MB). For high-volume workloads, increase this to reduce rotation overhead:
aws rds modify-db-parameter-group \
--db-parameter-group-name mysql80-audit \
--parameters \
"ParameterName=server_audit_file_rotate_size,ParameterValue=104857600,ApplyMethod=immediate" \
"ParameterName=server_audit_file_rotations,ParameterValue=10,ApplyMethod=immediate"The server_audit_file_rotations parameter controls how many rotated files are kept on the RDS local disk before the oldest is deleted. With 10 rotations at 100 MB each, the instance keeps 1 GB of audit history locally. Ensure your S3 export process consumes files before they are deleted — Lambda-based polling at 5-minute intervals is sufficient for 100 MB rotation files.
Aurora MySQL: Direct Audit to S3
If you are running Aurora MySQL 2.x or 3.x, the audit story is simpler. Aurora supports Advanced Auditing, which writes audit logs directly to S3 without routing through CloudWatch at all. Enable it by setting cluster-level parameters:
aws rds modify-db-cluster-parameter-group \
--db-cluster-parameter-group-name aurora-mysql80-audit \
--parameters \
"ParameterName=server_audit_logging,ParameterValue=ON,ApplyMethod=immediate" \
"ParameterName=server_audit_events,ParameterValue=CONNECT\,QUERY_DML\,QUERY_DDL,ApplyMethod=immediate"Aurora then exports audit logs to S3 automatically if you configure the cluster's log export configuration with audit enabled. Unlike RDS MySQL, Aurora does not write audit logs to CloudWatch by default — they go to an Aurora-managed S3 path that you can then mirror to your own bucket using an S3 replication rule. This eliminates the Firehose hop entirely, saving the $0.029/GB Firehose delivery cost.
Security Considerations
Audit logs are as sensitive as the data they describe — in some cases more so, because they reveal access patterns, user behavior, and authentication credentials in failed-login records. Apply the following controls to your S3 audit bucket without exception:
- Encryption at rest: Use SSE-KMS with a CMK. Rotate the CMK annually. Grant decrypt permissions only to the security team and your Athena execution role.
- Access control: Apply S3 Block Public Access at the account level. Use an S3 bucket policy with
aws:SourceAccountconditions on all write principals. Grant read access via IAM roles, not IAM users. - Object Lock: Enable S3 Object Lock in Compliance mode with a 365-day retention period. This prevents deletion of audit records even by root-level IAM principals, satisfying tamper-evident log requirements in SOC 2, PCI DSS, and HIPAA BAA.
- CloudTrail integration: Enable S3 data event logging for the audit bucket in CloudTrail. This creates a meta-audit trail showing who accessed or deleted audit records.
- VPC Endpoint: Route Firehose and Lambda S3 writes through a VPC Endpoint to prevent audit data from transiting the public internet.
Do not grant the Firehose IAM role s3:DeleteObject permission on the audit bucket. Firehose does not need it, and removing the permission eliminates the risk of accidental or malicious deletion of audit records through the delivery pipeline.
- CloudWatch Logs costs $0.50/GB ingested — for high-volume MySQL audit workloads, routing logs to S3 via Kinesis Firehose eliminates this cost entirely and replaces it with $0.029/GB delivery plus $0.023/GB storage.
- Use
QUERY_DML,QUERY_DDL,CONNECTIONinstead ofQUERYfor compliance logging — it satisfies SOC 2 and PCI DSS requirements at 5–10% of the log volume. - Partition S3 log paths by
year/month/dayand define Athena tables with matching partition columns — this reduces per-query Athena scan costs from dollars to cents. - Apply S3 Lifecycle rules to transition logs through STANDARD_IA, Glacier IR, and Deep Archive over 180 days, reducing per-GB monthly cost from $0.023 to $0.00099.
- Enable S3 Object Lock in Compliance mode to satisfy tamper-evident log requirements without relying on IAM policy alone.
- Aurora MySQL supports native audit-to-S3 without a CloudWatch intermediate step — if you are evaluating a migration from RDS MySQL, this alone can justify the move for audit-heavy workloads.
Working with JusDB on MySQL RDS Cost Optimization
JusDB works with engineering and DBA teams to audit their AWS RDS spend, identify over-provisioned logging configurations, and implement cost-optimized audit pipelines that remain fully compliant with SOC 2, PCI DSS, and HIPAA requirements. Our team handles the parameter group configuration, IAM policy construction, Firehose pipeline setup, Athena table definitions, and S3 lifecycle rules — delivering a tested, production-ready audit infrastructure without pulling your engineers off feature work.