Understanding scheduled queries concepts
Before creating scheduled queries, understand these key concepts that affect how your queries run and where results are delivered.
IAM role separation
Scheduled queries require two separate IAM roles: one for executing queries and another for delivering results to destinations such as Amazon S3 buckets, Amazon EventBridge event buses, or lookup tables. Understanding why this separation exists helps you configure permissions correctly and use the security and operational benefits it provides.
The two-role architecture divides responsibilities between data access and data delivery. The query execution role accesses your log data and runs queries, while the destination delivery role writes results to your chosen destination. This separation follows the principle of least privilege—each role has only the permissions it needs for its specific function.
- Query execution role
-
Allows CloudWatch Logs to run CloudWatch Logs Insights queries on your behalf. This role needs permissions to access your log groups and execute queries, but doesn't need access to destination resources. Required permissions:
-
logs:StartQuery -
logs:StopQuery -
logs:GetQueryResults -
logs:DescribeLogGroups -
logs:Unmaskif unmask data is required
For KMS-encrypted log groups:
kms:Decryptandkms:DescribeKeypermissions for the KMS key used to encrypt the log groups. These permissions need to be added as well.Trust relationship requirement: The query execution role must include a trust policy that allows the CloudWatch Logs service (
logs.amazonaws.com) to assume the role. Without this trust relationship, scheduled queries will fail with permission errors.Example trust policy for the query execution role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "logs.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }Example permissions policy for the query execution role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:StartQuery", "logs:StopQuery", "logs:GetQueryResults", "logs:DescribeLogGroups" ], "Resource": "*" } ] } -
- Destination delivery role
-
Allows CloudWatch Logs to deliver query results to your chosen destination. This role only needs permissions for the specific destination service, following the principle of least privilege. Required permissions vary by destination type.
Trust relationship requirement: The destination delivery role must also include a trust policy that allows the CloudWatch Logs service (
logs.amazonaws.com) to assume the role.Example permissions policy for S3 destination delivery role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": "arn:aws:s3:::your-scheduled-query-results-bucket/*" } ] }Example permissions policy for a lookup table destination delivery role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLookupTable", "logs:UpdateLookupTable", "logs:GetQueryResults" ], "Resource": "*" } ] }
This separation provides practical benefits for your operations. From a security perspective, if you need to change where results are delivered, you only modify the destination delivery role without changing the query execution permissions. For compliance and auditing, you can clearly track which role accesses sensitive log data and which role writes to external systems. This makes it easier to demonstrate that your log analysis infrastructure follows security best practices.
Cross-region and cross-account usage
A scheduled query is created in a specific region and runs in that region. However, you can query log groups and deliver results across regions and accounts. You need to set up one or more AWS accounts as monitoring accounts and link them with multiple source accounts. A monitoring account is a central AWS account that can view and interact with observability data generated from source accounts. A source account is an individual AWS account that generates observability data for the resources that reside in it. Source accounts share their observability data with the monitoring account. So you can setup scheduled queries from the monitoring account using the log groups of all linked accounts.
- Querying cross-region log groups
-
Your scheduled query can access log groups in any region. Specify log groups using their full ARN format:
arn:aws:logs:region:account-id:log-group:log-group-name. The query execution role needslogs:StartQueryandlogs:GetQueryResultspermissions for log groups in all target regions.
Important
When querying log groups or delivering results across regions, log data crosses regional boundaries. Consider the following:
-
Data residency requirements - Ensure cross-region data transfer complies with your organization's data governance policies and regulatory requirements
-
Data transfer costs - Cross-region data transfer incurs additional charges
-
Network latency - Queries accessing log groups in distant regions may experience higher latency
For optimal performance and cost efficiency, create scheduled queries in the same region as your primary log groups.
Alternative approach: Use CloudWatch Logs centralization to replicate log data from multiple accounts and regions into a central monitoring account. This allows you to create scheduled queries in a single region that access all your centralized logs, avoiding cross-region queries and simplifying IAM permissions management.
Schedule expressions and timezone handling
The schedule you define determines when your query runs and how often it executes. Choosing the right schedule expression affects when you receive results and how much data you query. Understanding the expression types helps you choose between simplicity and precision.
Cron expressions provide precise control over timing, allowing you to specify exact times, days of the week, or days of the month. Use cron expressions when you need queries to run at specific business hours or align with operational schedules. In the console you can also scheduled queries using easy calendar options.
- Cron expressions
-
Run queries at specific times. Format:
cron(minute hour day-of-month month day-of-week year). Examples:-
cron(0 9 * * ? *)- Every day at 9:00 AM UTC -
cron(0 18 ? * MON-FRI *)- Weekdays at 6:00 PM UTC -
cron(0 0 1 * ? *)- First day of every month at midnight UTC -
cron(0 12 ? * SUN *)- Every Sunday at noon UTC -
cron(30 8 1 1 ? *)- January 1st at 8:30 AM UTC
-
All scheduled queries run in UTC, regardless of your local timezone or where your AWS resources are located. This is particularly important when you schedule queries for business hours or time-sensitive analysis. For example, if your business operates in US Eastern Time and you want a daily report at 9 AM ET, you need to account for the UTC offset (14:00 UTC during daylight saving time, 13:00 UTC otherwise). Plan your schedule expressions with UTC in mind to ensure queries run at the intended times.
Choosing a query language
Scheduled queries support three different query languages, and your choice affects both how you write queries and how easily your team can maintain them. The right language depends on your analysis requirements and your team's existing skills.
If you are primarily filtering and aggregating log data, CloudWatch Logs Insights Query Language offers the most straightforward syntax. For complex data transformations where you need to reshape or enrich data through multiple steps, PPL's pipeline approach makes the logic easier to follow. When you need to perform joins or complex aggregations similar to database operations, SQL provides familiar syntax that database-experienced teams can adopt quickly.
- CloudWatch Logs Insights Query Language (CWLI)
-
Purpose-built for log analysis with intuitive syntax. Best for:
-
Text-based log analysis and filtering
-
Time-series aggregations and statistics
-
Teams new to log analysis
-
- OpenSearch Service Piped Processing Language (PPL)
-
Pipeline-based query language with powerful data transformation capabilities. Best for:
-
Complex data transformations and enrichment
-
Multi-step data processing workflows
-
Teams familiar with pipeline-based processing
-
- OpenSearch Service Structured Query Language (SQL)
-
Standard SQL syntax for familiar database-style queries. Best for:
-
Complex joins and aggregations
-
Business intelligence and reporting
-
Teams with strong SQL experience
-
Destination selection and use cases
Where you send query results determines what you can do with them. This choice shapes your entire downstream workflow—whether you are building long-term analytics, triggering automated responses, or both. Understanding the strengths of each destination type helps you design the right architecture for your use case.
Amazon S3 destinations are optimized for storage and batch processing. When you need to
keep query results for months or years, analyze trends over time, or feed data into
analytics platforms, Amazon S3 provides cost-effective storage with unlimited retention.
EventBridge destinations are optimized for real-time automation. When query results should
trigger immediate actions—like sending alerts, starting workflows, or updating
systems—EventBridge delivers results as events that your applications can respond to
instantly. By default all query completion events are automatically sent as events to the default event bus, enabling integration with downstream processing systems, Lambda functions, or other event-driven architectures. Results are only published to destinations when query is executed successfully.
Lookup table destinations are optimized for keeping reference data current. A
lookup table destination automatically populates or refreshes the specified lookup
table with the query results on each scheduled execution, so other queries can
reference the latest data with the lookup command.
- Amazon S3 destinations
-
Store query results as JSON files for long-term retention and batch processing. Amazon S3 destinations work best for the following scenarios:
-
Historical analysis and data archiving
-
Integration with data lakes and analytics platforms
-
Compliance and audit requirements
-
Cost-effective storage of large result sets
-
- EventBridge destinations
-
Send query results as events for real-time processing and automation. Use the
queryIdin the event to retrieve the query results, which remain available for 30 days after the query runs. EventBridge destinations work best for the following scenarios:-
Triggering automated responses to query results
-
Integration with serverless workflows and Lambda functions
-
Real-time alerting and notification systems
-
Event-driven architectures and microservices
-
- Lookup table destinations
-
Automatically create or refresh a lookup table with query results on each scheduled execution. Each refresh is a full replacement of the table content. Lookup table destinations work best for the following scenarios:
-
Keeping reference data current for the
lookupcommand in your log queries -
Maintaining allowlists, denylists, or entity inventories derived from log data
-
Enriching queries with recent activity summaries, such as active user or resource lists
-
Query result format and structure
Scheduled queries deliver results in JSON format, but each destination type receives a different payload. For a lookup table destination, the query results become the content of the lookup table, and each run replaces that content. For more information, see Configuring lookup table destinations for scheduled queries.
Amazon S3 destinations receive the result rows of the query. Each object contains a JSON
array with one entry for each row in the result set, and each entry maps the output
field names of the query to their values. The object does not contain query metadata
or query statistics, and it omits the @ptr field even if the query
requests it, because that field is usable only in the console.
EventBridge destinations receive query metadata, including the query statistics, but no
result rows. To retrieve the rows of a completed query, call GetQueryResults with the value of queryId from the
event.
The following example shows the event that CloudWatch Logs publishes to EventBridge when a scheduled query completes.
{ "version": "0", "id": "be72061b-eca2-e068-a7e1-83e01d6fe807", "detail-type": "Scheduled Query Completed", "source": "aws.logs", "account": "123456789012", "time": "2025-11-18T11:31:48Z", "region": "us-east-1", "resources": [ "arn:aws:logs:us-east-1:123456789012:scheduled-query:477b4380-b098-474e-9c5e-e10a8cc2e6e7" ], "detail": { "queryId": "2038fd57-ab4f-4018-bb2f-61d363f4a004", "queryString": "fields @timestamp, @message, @logStream\n| filter @message like /ERROR/\n| sort @timestamp desc\n| limit 10000", "logGroupIdentifiers": [ "/aws/lambda/my-function" ], "status": "Complete", "startTime": 1763465460, "statistics": { "recordsMatched": 1842, "recordsScanned": 48325, "estimatedRecordsSkipped": 0, "bytesScanned": 12081250, "estimatedBytesSkipped": 0, "logGroupsScanned": 1, "resultCount": 1842 } } }
This query does not aggregate, and it returned fewer rows than its
limit of 10,000, so each of the 1,842 matching log events became one
output row and recordsMatched and resultCount are equal. A
query that aggregates, or one whose result set is truncated by limit,
produces a resultCount lower than recordsMatched. For more
information, see Understanding recordsScanned, recordsMatched, and resultCount.
Key elements include:
-
statistics- Counters that describe how much log data the query read and how large the result set is. For a description of each field, see the following table. -
startTime- When the query execution started (Unix timestamp) -
queryString- The actual query that was executed -
queryId- Query id of the query using which results can be retrieved -
logGroupIdentifiers- List of log groups that were queried -
status- Query execution status (Complete, Failed, etc.)
The following table describes each field in the statistics object. For
the API definitions of these fields, see QueryStatistics.
| Field | Description |
|---|---|
recordsScanned |
The total number of log events scanned during the query. |
recordsMatched |
The number of log events that matched the query string. This value
counts log events, not output rows. For the number of rows in the
result set, use resultCount. |
resultCount |
The number of rows in the query result set. This value counts only
the rows that survived all operations in the query, so it might be
less than recordsMatched. It covers all pages of results
that GetQueryResults returns. For more information, see
Understanding recordsScanned, recordsMatched, and resultCount. |
estimatedRecordsSkipped |
An estimate of the number of log events that were skipped when processing this query, because the query contained an indexed field. Skipping these entries lowers query costs and improves the query performance time. For more information, see Create field indexes to improve query performance and reduce scan volume. |
bytesScanned |
The total number of bytes in the log events scanned during the query. |
estimatedBytesSkipped |
An estimate of the number of bytes in the log events that were skipped when processing this query, because the query contained an indexed field. |
logGroupsScanned |
The number of log groups that were scanned by this query. |
Understanding recordsScanned, recordsMatched, and resultCount
Three of the query statistics count different things, and comparing them directly can be misleading. Each one measures a different stage of query processing:
-
recordsScanned- The number of log events that the query read from your log groups. This is the input to the query. -
recordsMatched- The number of those log events that matched the query string. This value counts log events. -
resultCount- The number of rows in the result set that the query produced. This value counts output rows.
Each stage narrows the data. A command such as stats combines many log
events into a single output row, so resultCount can be much smaller than
recordsMatched. A large recordsMatched with a small
resultCount does not mean that rows are missing from the result
set.
Example – Aggregation
The following query counts the error messages in each log stream of one log group over a one-hour period.
filter @message like /ERROR/ | stats count(*) as errorCount by @logStream
If the query reads 1,500,000 log events, 24,318 of them contain ERROR,
and those matching log events come from 12 log streams, then the query completes with
the following statistics.
"statistics": { "recordsMatched": 24318, "recordsScanned": 1500000, "estimatedRecordsSkipped": 0, "bytesScanned": 450000000, "estimatedBytesSkipped": 0, "logGroupsScanned": 1, "resultCount": 12 }
stats produces one row for each log stream, so
resultCount is 12 while recordsMatched is 24,318. The 12
values of errorCount add up to 24,318.
Example – Post-aggregation filter
The following query keeps only the log streams that produced more than 1,000 errors.
filter @message like /ERROR/ | stats count(*) as errorCount by @logStream | filter errorCount > 1000
The final filter command runs after grouping, so it removes rows from
the result set instead of log events from the scan. If 3 of the 12 log streams have
an errorCount greater than 1,000, then resultCount is 3
instead of 12. recordsScanned and bytesScanned do not
change, because the query reads the same log data either way.
Example – Limit
A limit command reduces resultCount without any
aggregation. The following query returns the 100 most recent error messages.
filter @message like /ERROR/ | sort @timestamp desc | limit 100
If the query scans the same 1,500,000 log events and matches the same 24,318, then
resultCount is 100, because limit caps the result set at
100 rows. In the console, you see this relationship as Showing 100 of
24,318 records matched.
Each statistic answers a different question.
- How many rows did this query return?
-
Use
resultCount. A value of 0 means that the query produced no rows. Do not userecordsMatchedfor this purpose, because it counts log events rather than rows. - How much log data did this query read?
-
Use
recordsScannedandbytesScanned. Scan volume determines the cost and the run time of a query. To reduce it, shorten the time range, query fewer log groups, or create field indexes. For more information, see Create field indexes to improve query performance and reduce scan volume. - How many log events matched this query?
-
Use
recordsMatched.
Note
CloudWatch Logs omits a statistic from the statistics object when no value is
available for it, rather than reporting the statistic as 0.
If your event consumer does not find resultCount in an event,
treat the value as unknown rather than as 0. Write event consumers so that they
tolerate statistics that are absent and ignore statistics that they do not
recognize.