CloudWatch Logs Insights: Query Syntax and Examples
Last verified on Nov 8, 2026.
CloudWatch Logs Insights is the query tool built into Amazon CloudWatch Logs. You pick one or more log groups and a time range, write a short pipeline of commands, and get rows or a chart back. This guide covers the query language itself, worked examples for common log types, and the cost and limits that shape how you write queries. For Lambda-specific queries, see the Lambda logs guide.
How a Query Is Built
A query is a sequence of commands separated by the pipe character. Comments start with #. Every query runs against selected log groups for a chosen time range, and CloudWatch discovers some fields for you:
For logs from AWS services such as Lambda, CloudTrail, Route 53, and VPC flow logs, Insights also discovers service fields. For any log event that is JSON, it exposes keys as fields, with dot notation for nested objects such as request.userId. Everything else you extract yourself with parse.
Field discovery applies to log groups in the Standard log class. Log groups in the Essentials class support every command except pattern, diff, and unmask.
The Core Commands
Fields and Display
fields picks which fields appear and can compute new ones. display changes only what is shown.
fields @timestamp, @message, strlen(@message) as bytes
| sort @timestamp desc
| limit 25Filter
filter keeps events that match conditions. It supports =, !=, <, <=, >, >=, and, or, not, in [...], like, not like, and the regular expression operator =~. String matching is case sensitive.
filter level = "error" and durationMs > 2000 and path not like /health/If you have created a field index on a field, filter field = value and filter field in [...] can skip events that do not contain it. like never uses an index and always scans every event in the selected groups.
Parse
parse creates fields from text. It supports glob, regex, logfmt, and CSV modes. Events that do not match still appear in results, just without the new fields.
# Glob mode: one wildcard per alias
parse @message "user=* action=* ms=*" as user, action, ms
| stats avg(ms) by action
# Regex mode: named capture groups
parse @message /"(?<method>[a-zA-Z]+) (?<path>[^ ]+) [^"]*" (?<status>[0-9]{3})/
| filter status >= 500
| stats count(*) by path
# Logfmt mode: key=value lines become a map
parse @message logfmt as lf
| filter lf.level = "error"
| stats count(*) by lf.hostStats and Bin
stats aggregates. Add by bin(5m) to group by time and the console can draw a line chart. Without bin() you get a bar chart of up to 100 bars. Supported units run from milliseconds through years, and you can pluralize multi-character units.
filter level = "error"
| stats count(*) as errors by service, bin(5m)A query can have at most 10 stats commands, and any sort or limit must come after the last one.
Sort, Limit, and Dedup
sort orders results and limit caps them. If you omit limit, a query returns up to 10,000 events, and you can set it as high as 100,000. limit any 20 stops the scan as soon as 20 matches are found, which saves money but returns events in no particular order.
dedup keeps the first row for each distinct value in the order you sorted, so sort first. Events with a null value in the dedup field are kept.
fields @timestamp, host, message
| sort @timestamp desc
| dedup hostWorked Examples by Log Type
Application JSON logs, which routes return server errors and how slow they are:
filter status >= 500
| stats count(*) as failures, percentile(durationMs, 95) as p95 by route
| sort failures desc
| limit 15CloudTrail (see the CloudTrail integration), the most active API calls and who made them:
stats count(*) as calls by eventSource, eventName, userIdentity.arn
| sort calls desc
| limit 20CloudTrail, calls that failed:
fields @timestamp, eventName, errorCode, userIdentity.arn
| filter ispresent(errorCode)
| sort @timestamp descVPC flow logs (see the VPC flow logs integration), the biggest sources of rejected traffic:
filter action = "REJECT"
| stats sum(bytes) as rejectedBytes by srcAddr, dstPort
| sort rejectedBytes desc
| limit 10Unstructured logs, grouping errors by the exception name:
filter @message like /Exception/
| parse @message /(?<exception>[A-Za-z.]+Exception)/
| stats count(*) as occurrences by exception
| sort occurrences descComparing log streams, useful for finding the one bad host or container:
stats count(*) as events, sum(strcontains(@message, "timeout")) as timeouts by @logStream
| sort timeouts descRunning Queries from the CLI
The console is convenient, but aws logs start-query is what you script. Start and end times are epoch seconds, and you can name up to 50 log groups.
QID=$(aws logs start-query \
--log-group-names /aws/lambda/checkout /ecs/api \
--start-time $(date -d '-1 hour' +%s) \
--end-time $(date +%s) \
--query-string 'filter level = "error" | stats count(*) by bin(10m)' \
--query queryId --output text)
aws logs get-query-results --query-id "$QID"date -d is the GNU form. On macOS use date -v-1H +%s. get-query-results returns partial results while the status is Running, so poll until it reports Complete. You can page through up to 100,000 events with the nextToken.
What Queries Cost
Logs Insights bills on the amount of uncompressed log data scanned, not the number of rows returned. AWS's own guidance is to select only the log groups you need, use the narrowest time range, and cancel queries before closing the console page, since they keep running otherwise. A dashboard widget re-runs its query on every refresh, so a fast refresh interval multiplies cost. The current per-GB price is on AWS's pricing page, summarized in the CloudWatch pricing guide.
Two features help:
- estimate returns the approximate bytes a query would scan without running it, and costs nothing. Put it last:
fields @timestamp, @message | filter @message like /error/ | estimate. - Field indexes let equality filters skip events that cannot match. They work for structured JSON and service logs, and indexed fields are case sensitive.
Limits to Plan Around
If a query times out, shorten the time range or split it into several queries. For continuous processing rather than ad hoc analysis, subscription filters push events out as they arrive. See log aggregation and log parsing for the underlying ideas.
Querying CloudWatch Logs with Sazabi
Sazabi, an AI-native observability and security platform, can receive your CloudWatch log groups through a subscription filter, so you can ask questions in plain language instead of writing Insights queries. Connect the account under Settings > Log streams, then run sazabi logs ask "errors per service in the last 6 hours" from the CLI. The CloudWatch route is described in the connect-your-account docs and the CloudWatch integration page, and the log search docs cover the answer format.
Frequently Asked Questions
What language does CloudWatch Logs Insights use?
Its own query language, Logs Insights QL, made of commands such as fields, filter, parse, stats, sort, limit, and dedup joined with the pipe character. It also supports OpenSearch PPL and OpenSearch SQL.
How is CloudWatch Logs Insights billed?
By the amount of uncompressed log data a query scans, regardless of the query language. Narrower time ranges, fewer log groups, and field indexes all reduce what is scanned.
How do I extract a field from an unstructured log line?
Use parse with a glob such as parse @message "user=* status=*" as user, status, or with a regular expression and named capture groups. Lines that do not match stay in the results without the extracted fields.
How many results can a Logs Insights query return?
Up to 10,000 events by default, and up to 100,000 if you set limit explicitly. Queries stop after 60 minutes, and results stay available for 7 days.
Can I estimate what a query will cost before running it?
Yes. Add estimate as the last command. It returns the approximate number of bytes the query would scan and incurs no Logs Insights charge.
Sources
- Logs Insights overview: 100 concurrent queries per account, 60 minute query timeout, results kept 7 days, charges based on uncompressed data scanned (accessed Nov 8, 2026)
- Command list, pipe separator, hash comments, cost best practices, and Essentials log class command support (accessed Nov 8, 2026)
- filter operators, like and =~ matching, case sensitivity, and that only filter field = and IN use field indexes (accessed Nov 8, 2026)
- parse glob, regex, logfmt, and CSV modes, and that events that do not match stay in results without the extracted fields (accessed Nov 8, 2026)
- stats with bin(), supported time units, maximum of 10 stats commands, and sort and limit placement after the last stats (accessed Nov 8, 2026)
- dedup keeps the first result in sort order and keeps events with null values (accessed Nov 8, 2026)
- Default of 10,000 returned events, a maximum limit of 100,000, and limit any (accessed Nov 8, 2026)
- The estimate command returns bytes a query would scan without running it and incurs no query charge (accessed Nov 8, 2026)
- System fields @message, @timestamp, @ingestionTime, @logStream, and @log, JSON dot notation, and discovered fields for service logs (accessed Nov 8, 2026)
- start-query takes epoch-second start and end times, up to 50 log groups with --log-group-names, and a 100,000 maximum limit (accessed Nov 8, 2026)
- Field indexes support equality searches on structured JSON and service logs and skip events without the indexed value (accessed Nov 8, 2026)