The invoice lands. The Sentinel line is three times bigger than last month. You ask the team what changed, and the team answers as one: "Nothing." Somewhere, a firewall in debug mode quietly disagrees.
Rebuilt in my lab tenant with made-up data, checked against Microsoft Learn (October 2026). My lab: a Contoso workspace law-lab-sentinel in rg-lab-sentinel, a Windows client LAB-WIN11-01, and a CEF collector LAB-SYSLOG-01 fed by a made-up LabFirewall. All numbers are made-up lab numbers. No prices here, in any currency: they change by region and by month, so use the Microsoft Sentinel pricing page and its cost estimator.
TL;DR
- A bill jumps for one of four reasons: more billable GB, a higher price per GB (free trial ended, tier or benefit changed), longer retention, or a new meter (Basic, Auxiliary or data lake queries, search jobs, restore, notebooks). Cost Management grouped by Meter tells you which one.
- For volume, walk meter → day → table → source → event with the
Usagetable and the_BilledSizecolumn. Usually one table and one noisy source explain most of the jump. - Fix it at the source (DCR, transformation, device log level), move secondary data to the data lake tier, size the commitment tier, and add an alert. The daily cap is a seatbelt, not a diet.
At a glance
| Symptom | Sentinel cost is much higher than last month, and "nobody changed anything" |
| Why it happens | More billable GB, a higher rate per GB, longer retention, or a new meter on the bill |
| Fix | Find the meter, the day, the table and the source; cut the noise at the source; right-size tier, plan and retention |
| Prove it is fixed | Daily billable GB is back near the baseline, the meter line is flat in Cost analysis, an alert is in place |
| Files |
queries.kql, full text in Scripts
|
Table of contents
- 1. Symptom
- 2. Why it happens
- 3. Fix, step by step
- 4. Prove it is fixed
- Gotchas
- Checklist
- Scripts
- Sources
1. Symptom
You see one of these:
- The Sentinel part of the bill went up a lot, month over month.
- A budget alert fired mid-month.
- Cost analysis shows a flat line that suddenly became a hockey stick.
- Finance sends a screenshot with a red circle on it.
The lab scenario: law-lab-sentinel normally takes about 3 GB a day of billable data. On one day, the Windows Security Events DCR for LAB-WIN11-01 moves from Common to All events, and LabFirewall gets its log level raised "just for one test" (it stays there). Billable volume climbs to roughly 14 GB a day. Nobody thinks of either change as a "Sentinel change". That's the whole problem.
2. Why it happens
Sentinel charges by tier. The analytics tier is billed per GB ingested, pay-as-you-go or a commitment tier (formerly Capacity Reservations, from 100 GB a day). The data lake tier has its own meters: ingestion, data processing, storage, queries, and advanced data insights (notebooks). Then come retention and services like Logic Apps and Azure Functions that some connectors use.
So a bigger bill has four possible roots:
| Root | What it looks like | Typical cause |
|---|---|---|
| More GB | Ingestion meter climbs | New connector, verbose DCR, device in debug, more machines, duplicates from two agents |
| Higher rate | Same GB, bigger cost | 31-day free trial ended, workspace left a commitment tier, a free benefit stopped applying |
| Longer retention | Retention meter appears or grows | Analytics retention raised past 90 days, total retention in the lake raised past analytics retention |
| New meter | A line you never saw before | Queries on Basic or Auxiliary tables (billed per GB scanned), search jobs, restore, data lake queries and notebooks |
Names that changed, so your old notes still make sense:
- Simplified pricing. Workspaces that enabled Sentinel from July 2023 get one set of Sentinel meters that already includes Log Analytics ingestion. Older ones can still be on Classic tiers, with Sentinel and Log Analytics as separate lines. Two lines is normal there, not double billing.
- Archive is now long-term retention. After you onboard to the Sentinel data lake, long-term retention, search and Auxiliary ingestion move to the data lake meters. Azure Monitor now labels the plan Auxiliary / Lake.
- Free trial. The first 10 GB a day on the analytics tier is free for 31 days. Day 32 looks exactly like "the bill jumped".
- Defender portal. After March 31, 2027, Sentinel is no longer supported in the Azure portal. Tiers and retention for workspaces connected to Defender are managed in the Defender portal.
Some data is free: Azure Activity, Sentinel Health, Office 365 audit logs, and alerts from the Microsoft Defender products. The raw logs from Defender XDR, Entra ID and friends are paid. Free tables show IsBillable = false in Usage.
3. Fix, step by step
Step 1. Find the meter that moved
In Cost Management → Cost analysis, choose Daily costs, granularity Daily, Group by: Meter, and filter Service name to Sentinel, Log Analytics and Azure Monitor.
| Meter you see | What it means |
|---|---|
| Pay-as-you-go Analysis | Simplified pay-as-you-go ingestion. Climbing = more GB. |
n GB commitment tier plus Analysis
|
Commitment tier; Analysis is overage at the same effective rate |
| Classic ... plus Log Analytics Data Ingestion | Classic pricing, two lines by design |
| Basic Logs Analysis, Auxiliary Logs Analysis | Ingestion into those plans |
| Free trial Analysis, Free Benefit - M365 Defender ... | Free usage. If these disappear, your rate went up. |
Cost rose but billable GB didn't? Skip to step 7. A new retention or query meter? Skip to step 10.
Step 2. Find the day
Run query 1 in queries.kql (see Scripts) for the chart and query 2 for the table with ChangePercent. A jump from about 3 to 14 GB in one day points at a change on that day; a slow climb points at growth. Query 14 counts machines with a heartbeat per day: if that went up, you just have more machines.
Step 3. Find the table
Query 3 lists every table with IsBillable true or false, so the free ones step aside. Query 5 is my favourite: last 7 days against the 7 days before, sorted by the difference. In the lab scenario you should see SecurityEvent and CommonSecurityLog on top of DeltaGB, everything else near zero.
Query 6 splits billable GB by Plan (Analytics, Basic, Auxiliary). That column exists in Usage only since mid-May 2026, so older days show a blank. Don't panic about the blank.
Step 4. Find the source
Usage stops at the table level. To reach machines and resources, use two standard columns every row has: _IsBillable (is the row billed?) and _BilledSize (billed size in bytes).
Query 8 sums _BilledSize by computer for the last full day, query 9 by _ResourceId, and query 10 shows which tables one machine fills. In the lab scenario, LAB-WIN11-01 should lead in SecurityEvent and LAB-SYSLOG-01 in CommonSecurityLog.
These use find, which scans every table. Keep them to one day. Queries that touch Basic or Auxiliary tables are billed per GB scanned, so a curious find over a month is a fine way to make the bill jump again.
Step 5. Find the event
-
SecurityEvent: query 11 groups by
ComputerandEventID. With a DCR on All events, a handful of event IDs usually carry the bill. -
CommonSecurityLog: query 12 groups by
DeviceVendor,DeviceProductand collector, with the average row size. A device that sends lots of fields outside the standard CEF schema makes every row heavier, because those fields still land in the table. -
Syslog: query 13 groups by
Facility,SeverityLevelandProcessName. Debug-level syslog is chatty. Very chatty.
Step 6. Rule out duplicates and late data
- Two agents on one machine. The legacy Log Analytics agent is deprecated. A box that still runs it next to AMA can send the same event twice. See CEF and Syslog to Sentinel through AMA for the checks.
-
Late-arriving data. An agent that was offline sends its backlog when it reconnects, and
Usagecounts it on the day it arrived. Query 15 compares_TimeReceivedwithTimeGenerated. If most of the volume is stamped days earlier, the spike is a catch-up, not new noise.
Step 7. Check the rate, not just the volume
Same GB, bigger bill? Then the price per GB changed:
- Trial ended. The 31 free days are over.
- Commitment tier. Settings → Pricing in Sentinel shows the Current tier. Above the commitment you pay the same effective rate; below it, you still pay the full commitment.
-
Benefits. Query 16 charts free benefits used per day from the
Operationtable: the Defender for Servers allowance (500 MB per server per day for some security tables) and the Sentinel benefit for Microsoft 365 E5, A5, F5 and G5. If that line drops to zero on the day of the jump, a benefit stopped applying. Learn doesn't state the E5 amount; check the pricing page, not an old slide.
Step 8. Cut the noise at the source
The cheapest GB is the one you never send:
-
Windows Security Events. Set the DCR for
LAB-WIN11-01back from All events to Common or Minimal, or filter to the event IDs your rules use. AMA filters at the source, so dropped events never leave the machine. -
Syslog and CEF. Set the DCR facility and minimum log level to what your detections need, and ask the
LabFirewallowner to put logging back to normal. Bring coffee; this is a negotiation. - Transformations. Drop rows or columns you never query with an ingestion-time transformation in the DCR. For Analytics and Basic tables, filtering more than 50% normally adds a processing charge, but not when Sentinel is enabled on the workspace. Auxiliary tables pay data processing on all incoming data.
- Separate workspaces. Sentinel analyzes everything in its workspace, so operational data belongs elsewhere, unless the combined volume gets you into a commitment tier.
Step 9. Put secondary data in the data lake tier
Firewall, proxy, NetFlow and other high-volume, low-detection logs are "secondary security data". In the Defender portal, Data management → Tables, switch such a table from Analytics to Data lake, where ingestion and storage cost much less.
The catch: real-time analytics rules and hunting queries on that table stop working; you use KQL jobs, notebooks and summary rules instead. Check which rules read it first. (Basic tables are view-only in the Defender portal; manage them in the Log Analytics workspace.)
Step 10. Right-size tier and retention
- Commitment tier. The workspace's Usage and estimated costs page shows what the last 31 days would cost at each level. You can raise a tier any time (that restarts the 31-day period), but go down or back to pay-as-you-go only after 31 days. Size it for a normal day, not the spike day.
- Classic workspaces. Sentinel enabled before July 2023? Settings → Switch to new pricing moves you to simplified pricing. You can't switch back to classic in the portal afterwards, and if your account team negotiated a classic discount, talk to them first.
- Retention. The first 90 days of analytics retention are included for Sentinel; beyond that, long-term retention is charged. Defender XDR tables keep 30 days by default; extending them up to 90 days in the analytics tier costs ingestion but no storage. Lake retention beyond analytics retention is charged as lake storage. Ask, table by table: who reads this after 90 days?
Step 11. Add guardrails
- Alert on volume. A log search alert on query 18 with a threshold just above your normal day. Lab example: 6 GB.
-
Budget. A Cost Management budget on
rg-lab-sentinel, emailing people who will actually read it. - Daily cap, carefully. It stops billable Analytics and Basic collection for the rest of the day (Auxiliary isn't capped). The reset hour isn't configurable, data above the cap is still billed, and you're blind until the reset. Microsoft says plainly not to use it as your main cost control. If you set one, alert on query 17.
- Data lake limits. In the Defender portal, Microsoft Sentinel → Cost management (preview) adds threshold alerts and optional enforcement for lake queries and notebooks.
4. Prove it is fixed
Don't trust "it should be fine now". Trust these:
- The daily line is back. Queries 1 and 2 show billable GB near the baseline (about 3 GB a day in the lab scenario) for two full days after the change.
-
The noisy table shrank. Query 5 shows a negative
DeltaGBforSecurityEventandCommonSecurityLog. - Detections still work. Rules on those tables still return results. Saving money by deleting your own visibility is a different incident.
- The bill agrees. In Cost analysis grouped by Meter, the ingestion meter is flat again. Check a couple of days later, not an hour after the change.
- Guardrails are live. The volume alert and the budget exist, and a daily cap (if any) has its alert.
Gotchas
- "Nobody changed anything." Someone changed a DCR, a log level or a connector. Query 5 finds it faster than the meeting does.
- Classic looks like double billing. Two lines is how classic pricing works.
- The trial cliff. Day 32 looks exactly like a spike.
- A commitment is a commitment. Raising the tier restarts 31 days.
-
findcosts. One day at a time; Basic and Auxiliary queries bill per GB scanned. -
Units.
Quantityis MB,_BilledSizeis bytes. Mixing them makes a very scary chart. - Lake breaks real-time rules on that table. Check first.
- Daily cap = blind spot. Last-resort seatbelt only, with an alert.
Checklist
- [ ] Cost analysis grouped by Meter, the moving meter identified
- [ ] Day of the jump found (queries 1 and 2), machine count checked (query 14)
- [ ] Table that grew found (queries 3 and 5)
- [ ] Source found by computer or resource (queries 8 to 10)
- [ ] Event, device or facility found (queries 11 to 13)
- [ ] Duplicates and late-arriving data ruled out (query 15)
- [ ] Rate checked: trial, commitment tier, benefits (query 16)
- [ ] Noise cut at the source: DCR, device log level, transformation
- [ ] Secondary tables reviewed for the data lake tier, rules checked first
- [ ] Commitment tier and retention reviewed, classic vs simplified checked
- [ ] Volume alert, budget, and (if used) daily cap alert in place
What was the sneakiest thing that ever made your Sentinel bill jump? I'd like to add it to the gotchas.
Scripts
The full queries used in this post.
queries.kql
// SEN-02: Sentinel pricing, why the bill jumped
// Lab names (law-lab-sentinel, LAB-WIN11-01, LAB-SYSLOG-01). Replace them with your own.
// Units: Usage.Quantity is in MB (divide by 1000 for GB). _BilledSize is in bytes (divide by 1e9 for GB).
// Tip: the "TimeGenerated > ago(32d)" line only makes the portal look past its default 24 hours.
// StartTime and EndTime are the real hourly buckets in the Usage table.
// 1. Daily billable GB for the last 31 days: where is the jump?
Usage
| where TimeGenerated > ago(32d)
| where StartTime >= startofday(ago(31d)) and EndTime < startofday(now())
| where IsBillable == true
| summarize BillableGB = sum(Quantity) / 1000. by Day = bin(StartTime, 1d)
| render timechart
// 2. Same data as a table, with the day-over-day change, so you can name the day
Usage
| where TimeGenerated > ago(32d)
| where StartTime >= startofday(ago(31d)) and EndTime < startofday(now())
| where IsBillable == true
| summarize BillableGB = round(sum(Quantity) / 1000., 2) by Day = bin(StartTime, 1d)
| sort by Day asc
| extend PreviousDayGB = prev(BillableGB)
| extend ChangePercent = iff(PreviousDayGB > 0, round(100.0 * (BillableGB - PreviousDayGB) / PreviousDayGB, 1), real(null))
| sort by Day desc
// 3. Billable vs free, by table (DataType), last 31 days
// Free tables (AzureActivity, Heartbeat, Usage, Operation, SentinelHealth, Microsoft alerts) show IsBillable = false.
Usage
| where TimeGenerated > ago(32d)
| where StartTime >= startofday(ago(31d)) and EndTime < startofday(now())
| summarize GB = round(sum(Quantity) / 1000., 3) by DataType, IsBillable
| sort by GB desc
// 4. Top billable tables, daily, as a stacked chart (one colour per table)
Usage
| where TimeGenerated > ago(32d)
| where StartTime >= startofday(ago(31d)) and EndTime < startofday(now())
| where IsBillable == true
| summarize BillableGB = sum(Quantity) / 1000. by bin(StartTime, 1d), DataType
| render columnchart
// 5. Which table grew? Last 7 full days vs the 7 days before
let ThisWeek = Usage
| where TimeGenerated > ago(8d)
| where StartTime >= startofday(ago(7d)) and EndTime < startofday(now())
| where IsBillable == true
| summarize ThisWeekGB = sum(Quantity) / 1000. by DataType;
let LastWeek = Usage
| where TimeGenerated > ago(15d)
| where StartTime >= startofday(ago(14d)) and EndTime < startofday(ago(7d))
| where IsBillable == true
| summarize LastWeekGB = sum(Quantity) / 1000. by DataType;
ThisWeek
| join kind=fullouter LastWeek on DataType
| extend DataType = coalesce(DataType, DataType1)
| extend ThisWeekGB = round(coalesce(ThisWeekGB, 0.0), 2), LastWeekGB = round(coalesce(LastWeekGB, 0.0), 2)
| extend DeltaGB = round(ThisWeekGB - LastWeekGB, 2)
| project DataType, LastWeekGB, ThisWeekGB, DeltaGB
| sort by DeltaGB desc
// 6. Billable GB by table plan (Analytics, Basic, Auxiliary)
// The Plan column was added to Usage in mid-May 2026. Older rows are empty.
Usage
| where TimeGenerated > ago(32d)
| where StartTime >= startofday(ago(31d)) and EndTime < startofday(now())
| where IsBillable == true
| summarize BillableGB = round(sum(Quantity) / 1000., 2) by Plan, DataType
| sort by BillableGB desc
// 7. Billable GB by solution (SecurityInsights = Sentinel)
Usage
| where TimeGenerated > ago(32d)
| where StartTime >= startofday(ago(31d)) and EndTime < startofday(now())
| where IsBillable == true
| summarize BillableGB = round(sum(Quantity) / 1000., 2) by Solution
| extend Solution = iff(Solution == "SecurityInsights", "AzureSentinel", Solution)
| sort by BillableGB desc
// 8. Top computers by billable GB, last full day
// "find" scans every table. Keep it to one day. Queries that touch Basic or
// Auxiliary tables are billed per GB scanned, so be careful on big workspaces.
find where TimeGenerated between (startofday(ago(1d)) .. startofday(now())) project _BilledSize, _IsBillable, Computer, Type
| where _IsBillable == true and Type != "Usage"
| extend ComputerName = tolower(tostring(split(Computer, '.')[0]))
| where ComputerName != ""
| summarize BillableGB = round(sum(_BilledSize) / 1e9, 3) by ComputerName
| sort by BillableGB desc nulls last
| take 20
// 9. Top Azure resources by billable GB, last full day (_ResourceId)
find where TimeGenerated between (startofday(ago(1d)) .. startofday(now())) project _ResourceId, _BilledSize, _IsBillable
| where _IsBillable == true
| summarize BillableGB = round(sum(_BilledSize) / 1e9, 3) by _ResourceId
| sort by BillableGB desc nulls last
| take 20
// 10. One computer, which tables? (change the name)
find where TimeGenerated between (startofday(ago(1d)) .. startofday(now())) project _BilledSize, _IsBillable, Computer, Type
| where Computer startswith "LAB-WIN11-01"
| where _IsBillable == true
| summarize BillableGB = round(sum(_BilledSize) / 1e9, 3), Rows = count() by Type
| sort by BillableGB desc
// 11. Drill into SecurityEvent: which event IDs cost the most? (last 7 days)
SecurityEvent
| where TimeGenerated > ago(7d)
| where _IsBillable == true
| summarize Rows = count(), BilledGB = round(sum(_BilledSize) / 1e9, 3) by Computer, EventID
| sort by BilledGB desc
| take 25
// 12. Drill into CommonSecurityLog: which device, which collector, how big is each row?
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where _IsBillable == true
| summarize Rows = count(),
BilledGB = round(sum(_BilledSize) / 1e9, 3),
AvgRowBytes = round(avg(_BilledSize), 0)
by DeviceVendor, DeviceProduct, Computer
| sort by BilledGB desc
// 13. Drill into Syslog: which facility, severity and process?
Syslog
| where TimeGenerated > ago(7d)
| where _IsBillable == true
| summarize Rows = count(), BilledGB = round(sum(_BilledSize) / 1e9, 3) by Computer, Facility, SeverityLevel, ProcessName
| sort by BilledGB desc
| take 25
// 14. Did more machines start sending? Nodes with a heartbeat, per day
Heartbeat
| where TimeGenerated > startofday(ago(31d))
| summarize Nodes = dcount(Computer) by bin(TimeGenerated, 1d)
| render timechart
// 15. Late-arriving data: rows received on one day but stamped with older times
// Change the table and the date to the day of your jump.
SecurityEvent
| where TimeGenerated > datetime(1970-01-01)
| where _TimeReceived >= datetime(2026-10-14) and _TimeReceived < datetime(2026-10-15)
| where _IsBillable == true
| summarize BilledMB = round(sum(_BilledSize) / 1e6, 1) by bin(TimeGenerated, 1d)
| sort by TimeGenerated asc
// 16. Free data benefits used per day (Defender for Servers allowance, Sentinel benefit for Microsoft 365 E5/A5/F5/G5)
// If this line drops to zero on the day the bill jumps, a benefit stopped applying.
Operation
| where TimeGenerated >= ago(31d)
| where Detail startswith "Benefit amount used"
| parse Detail with "Benefit amount used: " BenefitUsedGB " GB"
| extend BenefitUsedGB = toreal(BenefitUsedGB)
| parse OperationKey with "Benefit type used: " BenefitType
| project BillingDay = TimeGenerated, BenefitType, BenefitUsedGB
| sort by BillingDay asc, BenefitType asc
| render columnchart
// 17. Was the daily cap hit? (collection stops until the workspace reset hour)
_LogOperation
| where TimeGenerated > ago(31d)
| where Category =~ "Ingestion"
| where Detail contains "OverQuota"
| project TimeGenerated, Operation, Detail
// 18. Alert query: billable GB in the last 24 hours (use in a log search alert rule)
// Set the threshold a bit above your normal day. Lab example: alert above 6 GB.
Usage
| where IsBillable
| summarize DataGB = sum(Quantity / 1000)
Sources
- Plan costs and understand pricing and billing for Microsoft Sentinel
- Manage and monitor costs for Microsoft Sentinel
- Reduce costs for Microsoft Sentinel
- Enroll in a simplified pricing tier for Microsoft Sentinel
- Log retention tiers in Microsoft Sentinel
- Manage data tiers and retention in Microsoft Sentinel
- Microsoft Sentinel data lake overview
- Analyze usage in a Log Analytics workspace
- Azure Monitor Logs cost calculations and options
- Azure Monitor cost and usage
- Azure Monitor Logs overview: table plans
- Standard columns in Azure Monitor log records
- Usage table reference
- CommonSecurityLog table reference
- Transformations in Azure Monitor
- Set a daily cap on a Log Analytics workspace
- Microsoft Sentinel pricing

Top comments (0)