Zillow cuts more than 500 jobs in its largest layoff of the year
Zillow Group laid off more than 500 employees Tuesday, about 7% of its workforce, one day before it reports second-quarter earnings. … Read More
At endjin, we maintain Corvus.JsonSchema, an open source high-performance JSON library for .NET. We generate API reference documentation for 16 libraries across two engine versions (V4 and V5), spanning JSON Schema validation, query languages (JSONata, JMESPath, JsonLogic, JSONPath), mutable documents, YAML conversion, and JSON Patch. (We deliberately exclude the 7 V4 JSON Schema dialect libraries - Draft 4/6/7/201909/202012/OpenApi30/31 - because they contain thousands of generated types with repetitive patterns that would add ~25,000 pages and make the build impractically slow.)
That's a lot of public API surface to document. We needed API reference documentation that would stay accurate as the code evolved, support both engine versions side by side, and be enriched with hand-written examples and descriptions. It also needed to avoid requiring a human to manually update about 8,800 pages every time a method signature changed.
This post describes the system we built: a custom documentation pipeline that generates API reference directly from compiled assemblies and XML doc comments, produces a searchable static site with source links and TFM availability badges, and runs as part of our CI build.
Before building anything, we listed what we needed:
Corvus.Text.Json.Patch that takes a JsonElement defined in Corvus.Text.Json).netstandard2.0 vs netstandard2.1 vs net10.0, since the library multi-targets and some APIs are only available on newer frameworks.We evaluated existing tools. The combination of multi-assembly cross-linking, dual-version page generation, TFM badge scanning, and regeneration-safe enrichment we needed would have required substantial customisation of any off-the-shelf solution. Since we already had a static site generator (Vellum), we decided to build a focused tool that did exactly what we needed.
The heart of the system is XmlDocToMarkdown, a C# console application that reads XML documentation files and compiled assemblies, and produces the artefacts needed to render API reference pages.
For each library, the tool receives three inputs:
XML documentation file (Corvus.Text.Json.xml) - the standard output from <GenerateDocumentationFile>true</GenerateDocumentationFile>, containing <summary>, <param>, <returns>, <remarks>, <example>, and <exception> elements for every documented member.
Compiled assembly (Corvus.Text.Json.dll, net10.0 build) - inspected via System.Reflection to discover the actual public API surface: types, members, generic constraints, inheritance, interface implementations.
Companion assemblies (netstandard2.0 and netstandard2.1 builds, optional) - scanned to determine which types and members are available on each target framework.
For multi-assembly documentation, these triplets are repeated. The V5 build step constructs the argument pairs for all 8 libraries:
$v5ToolArgs = @()
foreach ($proj in $v5Projects) {
$binDir = Join-Path $v5SrcDir "$proj\bin\Release\net10.0"
$xmlFile = Join-Path $binDir "$proj.xml"
$dllFile = Join-Path $binDir "$proj.dll"
$ns20Dll = Join-Path $v5SrcDir "$proj\bin\Release\netstandard2.0\$proj.dll"
$ns21Dll = Join-Path $v5SrcDir "$proj\bin\Release\netstandard2.1\$proj.dll"
if ((Test-Path $xmlFile) -and (Test-Path $dllFile)) {
$v5ToolArgs += "--xml", $xmlFile, "--assembly", $dllFile
if (Test-Path $ns20Dll) { $v5ToolArgs += "--ns20-assembly", $ns20Dll }
if (Test-Path $ns21Dll) { $v5ToolArgs += "--ns21-assembly", $ns21Dll }
}
}
From those inputs, the tool generates several kinds of output:
| Output | Purpose | V5 count |
|---|---|---|
| Namespace markdown | One page per namespace with a type listing table | 21 |
| Type markdown | One page per public type with signature, docs, member tables | ~800 |
| Member markdown | One page per method overload group, property, operator, etc. | ~2,500 |
| Taxonomy YAML | Metadata for Vellum (our static site generator) to route and render each page | ~3,300 |
| Razor views | API index page with namespace cards, hierarchical sidebar partial | 2 |
| Search index | JSON file consumed by Lunr for per-version type and member search | 1 |
The output directory has a flat file structure, with naming conventions that encode the namespace and type hierarchy:
Api-v5/
├── corvus-numerics.md # Namespace page
├── corvus-numerics-bignumber.md # Type page
├── corvus-numerics-bignumber.parse.md # Member page (method)
├── corvus-numerics-bignumber.op-addition.md # Member page (operator)
├── corvus-text-json.md # Namespace page
├── corvus-text-json-jsonelement.md # Type page
├── corvus-text-json-jsonelement.clone.md # Member page
├── corvus-text-json-jsonelement.createbuilder.md # Member page
├── ...
├── namespaces/
│ ├── Corvus.Numerics.md # Hand-authored namespace description
│ ├── Corvus.Text.Json.md # (survives regeneration)
│ └── ... # 21 files
├── examples/
│ ├── corvus-text-json-jsonelement.md # Hand-authored type example
│ └── ... # 25 files
└── sidebar.html # Pre-rendered sidebar fragment
Each generated type page includes the full signature, XML doc summary, member tables with links, and (where available) source links and hand-authored examples. For example, the generated page for JsonElement.Clone looks like this:
## Definition
**Namespace:** Corvus.Text.Json
**Assembly:** Corvus.Text.Json.dll
**Source:** [JsonElement.cs](https://github.com/.../JsonElement.cs#L2288)
## Clone() {#clone}
Get a JsonElement which can be safely stored beyond the lifetime
of the original JsonDocument.
```csharp
public JsonElement Clone()
```
### Returns
[`JsonElement`](/api/v5/corvus-text-json-jsonelement.html)
A JsonElement which can be safely stored beyond the lifetime
of the original JsonDocument.
Each page also gets a companion taxonomy YAML file that tells the static site generator how to route and render it:
ContentType: application/vnd.endjin.ssg.page+yaml
Title: "JsonElement"
Template: api/v5/api-page
Navigation:
Title: "JsonElement"
Description: "Represents a specific JSON value within a JsonDocument."
Parent: /api/v5
Url: /api/v5/corvus-text-json-jsonelement.html
Rank: 109
ContentBlocks:
- ContentType: application/vnd.endjin.ssg.content+md
Spec:
Path: ../../content/Api-v5/corvus-text-json-jsonelement.md
In total, a single pipeline run produces about 8,800 API reference pages. That includes 3,300 for V5 and 5,500 for V4.
The documentation build script orchestrates everything in a single PowerShell pipeline:
| Step | What it does |
|---|---|
| 0 | Copy hand-authored source files (overviews, taxonomy seeds) |
| 1a | Build 8 V5 libraries (Release, net10.0 + netstandard2.0 + netstandard2.1) |
| 1b | Build 8 V4 libraries (Release, net10.0 + netstandard2.0) |
| 2a | Generate V5 API pages (markdown, taxonomy, views, search index) |
| 2b | Generate V4 API pages |
| 3 | Generate recipe content from 42 ExampleRecipes |
| 4 | Generate docs content from source documentation via descriptors |
| 5 | Install Vellum SSG |
| 6 | Run Vellum to render the core site |
| 7 | Compile SCSS and copy API search indices/sidebars |
| 8 | Build site-wide Lunr search index |
| 9 | Build and publish interactive playgrounds |
| 10 | Check for broken links (lychee) |
| 11 | Rewrite root-relative paths for GitHub Pages subpath hosting |
The link checker (step 10) runs before step 11's path rewriting, so root-relative links like /api/v5/corvus-text-json-jsonelement.html resolve directly against the .output/ directory structure. Any broken internal link fails the build.
The documentation build is integrated into our CI pipeline as a PostBuild task:
task PostBuild BuildWebSiteLocal
task BuildWebsite {
$websiteDir = Join-Path $here "docs\website"
$websiteBuildArgs = @{ SkipDotNetBuild = $true }
if ($VellumDownloadToken) {
$websiteBuildArgs += @{ VellumDownloadToken = (ConvertTo-SecureString $VellumDownloadToken -AsPlainText) }
}
$basePathPrefix = $env:BUILDVAR_BasePathPrefix
if ($basePathPrefix) {
$websiteBuildArgs += @{ BasePathPrefix = $basePathPrefix }
}
& (Join-Path $websiteDir "build.ps1") @websiteBuildArgs
}
task BuildWebSiteLocal -If { $BuildWebsite } BuildWebsite
The BuildWebSiteLocal wrapper means the website only builds when $BuildWebsite is set. You pass this flag explicitly for local documentation builds. In CI, the BuildWebsite task is invoked directly by the workflow. Either way, -SkipDotNetBuild means it reuses the already-compiled binaries from the main build step.
We also run a separate documentation code sample catalog check in our PreBuild task, which catches drift between documentation markdown and its code sample inventory before the build even starts.
To make this concrete, here's what happens when someone adds a new public method to JsonElement:
dotnet build produces the updated .dll and .xml.XmlDocToMarkdown tool runs. It loads all 8 V5 assembly/XML pairs, then inspects each assembly.JsonElement, it finds the new method in the assembly metadata, matches it to its XML doc entry, and checks whether the method exists in the netstandard2.0 and netstandard2.1 builds.corvus-text-json-jsonelement.md (the type page) with the new method in its member table, and creates or updates corvus-text-json-jsonelement.{method-slug}.md (the member detail page) with the full signature, parameter docs, return type, exceptions, and source link.examples/corvus-text-json-jsonelement.md is loaded and merged into the type page - untouched by the regeneration.No one manually edited a documentation page. The developer wrote code and XML doc comments. Everything else was automated.
So that's the shape of the system: XML docs and compiled assemblies go in, a fully navigable documentation site comes out, and hand-authored enrichments survive regeneration. If you want to adapt the approach for your own project, the XmlDocToMarkdown source and the build pipeline are the places to start.
In Part 2, we'll go under the hood: how the cross-assembly linking, PDB-based source links, TFM scanning, enrichment merging, and search indexing actually work.
There's a moment when you're deep into an agentic coding session and you have to leave your desk (time to catch my train!). Normally that means the session just sits there, waiting, until you're back at your keyboard. Both Claude Code and GitHub Copilot CLI recently shipped a feature that fixes exactly this: you keep the session running locally, but you can check in, approve tool calls, and keep steering it from your phone or any browser.
Let's look at how each one works.
/remote-controlIn Claude Code, you enable this with a slash command inside a running session:
/remote-control
or the short form:
/rc
You can also start a session already remote-enabled:
claude --remote-control
or run a dedicated server process that can host multiple concurrent sessions:
claude remote-control
Whichever way you start it, Claude Code prints a session URL (and lets you press spacebar to show a QR code) that connects to claude.ai/code or the Claude app. From there you get a live view of the terminal: connection status, tool activity, and the ability to send messages back in.
Remark: the architecture is worth understanding. Your local Claude Code process makes an outbound HTTPS connection to the Anthropic API and polls it for instructions — it never opens an inbound port. Your phone doesn't execute anything or touch your files directly; it just renders the conversation and sends prompts. Execution, file access, and MCP servers all stay on your machine.
A few practical details:
claude remote-control again. /plugin or /resume, are local-only — they won't work from a remote client. claude remote-control --spawn worktree gives each on-demand session its own git worktree, so parallel sessions don't fight over the same files. You switch between them in the Claude app like chat threads, with push notifications when one finishes or needs a decision. /remoteCopilot CLI has the same idea, just named differently. Inside a running session:
/remote on
Or start the session already remote-enabled:
copilot --remote
Or make it the default for every interactive session by adding this to ~/.copilot/settings.json:
{
"remoteSessions": true
}
(Override that per session with --no-remote.)
Once enabled, Copilot CLI prints a link to the session on GitHub.com.
Sign in with the same account that started the session and you get a live, steerable view, not just read-only output.
You can also find it without the link: GitHub.com → Copilot icon → "Agent sessions," or the Agents tab of the repo you started the session in.
Remark: don't confuse this with regular session syncing. Copilot CLI sessions sync to your GitHub account by default and show up as view-only on GitHub.com and GitHub Mobile but you can't steer those. Only sessions with remote control explicitly enabled are steerable.
I asked a question on the Github website:
And the agent starts to work on it locally:
On mobile, it's the same experience via GitHub Mobile: tap the Copilot button, find your session under "Agent sessions." For quick access, run /remote in the session to redisplay the details, then press Ctrl+E to toggle a QR code.
One thing Copilot CLI has that I like: /keep-alive. Your laptop going to sleep kills the session just like it would with Claude Code, so you can tell it explicitly to stay awake:
/keep-alive on # never sleep while the session is active
/keep-alive busy # only stay awake while Copilot is actively working
/keep-alive 8h # stay awake for a fixed duration
And if you resume a session later with copilot --continue or copilot --resume, remote control is automatically re-enabled. You don't need to pass --remote again.
Both tools solve the same problem the same way: execution stays local, only the conversation travels.
/remote-control (or /rc) in Claude Code, /remote on in Copilot CLI. --remote-control vs. --remote. /config, or "remoteSessions": true in Copilot CLI's settings.json. claude.ai/code or the Claude app for Claude Code; GitHub.com or GitHub Mobile for Copilot CLI. /keep-alive command with fine-grained durations. Which one you reach for probably just comes down to which tool you're already running. If you're bouncing between both like I do, it's worth knowing that both tools have this functionality built-in.
That's it! Two different agent harnesses, same remote control idea.
A better fire alarm is still a fire. Many SQL Server incidents I get called about were preventable with controls already available in the client’s environment. Here is the audit I wish had been run before the pager rang, complete with the T-SQL.

I finished a root cause analysis recently that I was quietly proud of. It had everything a respectable incident document is supposed to have: a timeline to the second, the plan regression that started it, the parameter behind the regression, and the deployment three weeks earlier that changed the parameter.
The document was accurate. That was the uncomfortable part. Not one important fact required hindsight. Every warning had existed before the outage. I had not solved a mystery. I had written an excellent account of an avoidable event.

So this post is the other document, the one that should exist before the incident. These are eight checks I now run on every engagement, why each one matters, and the T-SQL to see where you stand. The first pass takes about an hour. Testing and implementing any change still belongs in normal change control.
A quick note. Details are blended across engagements and changed so nothing identifies a client. Test everything below outside production first, as you would with anything you read on the internet.
There is an easy way to ruin this audit before it begins: assume every edition can do every trick. Record the exact version and edition first. Automatic tuning remains an Enterprise feature in boxed SQL Server. Developer edition through SQL Server 2022, and Enterprise Developer in SQL Server 2025, provide the Enterprise feature set for non-production development. Resource Governor is available in Enterprise and Developer through SQL Server 2022. SQL Server 2025 makes it available in Enterprise, Enterprise Developer, Standard, and Standard Developer. Azure SQL offerings have different support rules.
SELECT SERVERPROPERTY('ProductVersion') AS product_version,
SERVERPROPERTY('ProductMajorVersion') AS major_version,
SERVERPROPERTY('Edition') AS edition,
SERVERPROPERTY('EngineEdition') AS engine_edition;
The audit queries are read-only, but read-only does not mean permission-free. Requirements vary by view and SQL Server version. Common permissions include VIEW DATABASE STATE or VIEW DATABASE PERFORMANCE STATE for database-scoped information, and VIEW SERVER STATE or VIEW SERVER PERFORMANCE STATE for instance-scoped information. Changing a setting requires separate permission and change approval.
Checks 1 and 2 are database-scoped. The detailed statistics and table-footprint queries in checks 5 and 6 are also database-scoped, so run them in each writable user database you intend to audit. The remaining queries examine instance-wide configuration, tempdb, Resource Governor, or all online user databases.
| Check | Failure class | First evidence to collect |
|---|---|---|
| 1 | Plan regression | Automatic tuning state and current recommendations |
| 2 | Missing performance history | Query Store state, capacity, and capture freshness |
| 3 | Parallel worker pressure | Cost threshold, MAXDOP, waits, and workload evidence |
| 4 | tempdb allocation contention |
File layout and sustained allocation-page waits |
| 5 | Cardinality estimate drift | Statistics settings, freshness, sampling, and plan estimates |
| 6 | Large-table deployment risk | Row count, space, lock impact, log impact, and rollback |
| 7 | Workload collision | Resource Governor policy and session classification |
| 8 | Slow-building outage conditions | Sustained blocking and transaction-log pressure |

This is the one that annoys me most on supported editions. The capability has existed since SQL Server 2017, yet I still meet systems where nobody has even checked its state. Enabling it is one statement. Deciding to enable it is still a change, and those are not the same thing.
When SQL Server identifies an eligible query plan choice regression, automatic plan correction can force the last known good plan, verify the result, and undo the force if performance does not improve. It does not catch every regression, but it can make a useful class of plan incidents self-correcting.
SELECT name,
desired_state_desc,
actual_state_desc,
reason_desc
FROM sys.database_automatic_tuning_options;
If actual_state_desc is OFF, do not jump straight to the ALTER statement. Read reason_desc, confirm that the version and edition support the feature, and verify that Query Store is healthy. A single command can enable the feature, but it cannot create the history the feature needs.
ALTER DATABASE CURRENT
SET AUTOMATIC_TUNING (FORCE_LAST_GOOD_PLAN = ON);
Automatic plan correction requires Query Store to be read-write, which is check 2. After a week, ask the database for receipts instead of assuming that ON means useful:
SELECT type,
reason,
score,
execute_action_initiated_by,
execute_action_initiated_time,
revert_action_initiated_by,
revert_action_initiated_time,
JSON_VALUE(state, '$.currentValue') AS current_state,
JSON_VALUE(state, '$.reason') AS state_reason,
JSON_VALUE(details, '$.planForceDetails.queryId') AS query_id,
JSON_VALUE(details, '$.planForceDetails.regressedPlanId') AS regressed_plan,
JSON_VALUE(details, '$.planForceDetails.recommendedPlanId') AS recommended_plan,
JSON_VALUE(details, '$.implementationDetails.script') AS script
FROM sys.dm_db_tuning_recommendations
ORDER BY score DESC;
The score is the estimated value or effect of the recommendation on a scale from 0 to 100, with larger values considered better. If you are not ready to enable automatic correction, leave it off and review this DMV for a fortnight. SQL Server can still identify potential regressions when the option is disabled. Treat each recommendation as a lead to investigate, not a guaranteed future outage. The DMV is not persisted, so a Database Engine restart clears its recommendations. If the history matters, collect it elsewhere.
SQL Server 2022 enables Query Store by default for newly created databases. Databases restored from earlier versions, and databases carried through an in-place upgrade, retain their previous setting. In every case, on is not the same as working.
The failure I find most often is a Query Store sitting in READ_ONLY because its storage filled months ago. It stopped collecting quietly, nobody was alerted, and the first person to notice is the person who desperately needs yesterday’s history. That is a terrible time to discover that the security camera has not been recording.

SELECT actual_state_desc,
desired_state_desc,
readonly_reason,
current_storage_size_mb,
max_storage_size_mb,
query_capture_mode_desc,
size_based_cleanup_mode_desc,
stale_query_threshold_days
FROM sys.database_query_store_options;
On a database with an active workload, also confirm that Query Store contains recent runtime data:
SELECT MAX(last_execution_time) AS latest_captured_execution
FROM sys.query_store_runtime_stats;
A null or unexpectedly old timestamp on a busy database is a reason to inspect the capture policy and Query Store health. Interpret it against the workload and collection interval, because an idle database should not produce fresh executions.
If actual_state_desc and desired_state_desc disagree, something forced it read-only and readonly_reason tells you what. It is a bitmask. 65536 means the storage size was exceeded, which is the common one.
Here is a sensible starting configuration, not a universal recipe. Adjust the capacity and retention to the workload, then monitor them:
ALTER DATABASE CURRENT SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
MAX_STORAGE_SIZE_MB = 2048,
QUERY_CAPTURE_MODE = AUTO,
SIZE_BASED_CLEANUP_MODE = AUTO,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 60),
MAX_PLANS_PER_QUERY = 200,
INTERVAL_LENGTH_MINUTES = 60,
DATA_FLUSH_INTERVAL_SECONDS = 900
);
Two notes matter. QUERY_CAPTURE_MODE = ALL can fill storage with single-execution ad hoc noise on some workloads. Also, put an alert on the state. A red row in an occasional audit is not an alerting strategy:
IF DATABASEPROPERTYEX(DB_NAME(), 'Updateability') = 'READ_WRITE'
AND EXISTS (SELECT 1 FROM sys.database_query_store_options
WHERE actual_state_desc <> 'READ_WRITE')
RAISERROR('Query Store is not recording', 16, 1);
Run that check as a SQL Server Agent job step and configure the job to notify somebody when the step fails. RAISERROR by itself does not send an email.
While you are here, audit what is already being forced, including plans with recorded forcing failures:
SELECT p.query_id,
p.plan_id,
p.is_forced_plan,
p.plan_forcing_type_desc,
p.force_failure_count,
p.last_force_failure_reason_desc
FROM sys.query_store_plan AS p
WHERE p.is_forced_plan = 1
OR p.force_failure_count > 0;
A nonzero force_failure_count means plan forcing has failed at least once. The counter increments when forcing fails during recompilation, not on every execution. Inspect last_force_failure_reason_desc, the current plan, and recent compilations before assuming that the query is still protected.
SELECT name, value_in_use
FROM sys.configurations
WHERE name IN ('cost threshold for parallelism',
'max degree of parallelism');
Cost threshold still defaults to 5. Five is a factory default, not a recommendation and certainly not a family tradition. SQL Server considers parallel alternatives when the best serial plan’s estimated cost exceeds the threshold. That cost is an optimizer estimate, not seconds. If the threshold is too low for an OLTP workload, too many modest queries can receive parallel plans and contribute to worker pressure. CXPACKET and CXCONSUMER waits are evidence to interpret, not a diagnosis by themselves.

DECLARE @target_cost_threshold int = 20; -- Example only. Choose from evidence.
DECLARE @apply_change bit = 0; -- Change to 1 only after approval.
SELECT value_in_use AS current_value,
@target_cost_threshold AS proposed_value,
@apply_change AS apply_change
FROM sys.configurations
WHERE name = 'cost threshold for parallelism';
IF @apply_change = 0
RETURN;
IF @target_cost_threshold NOT BETWEEN 0 AND 32767
THROW 50000, 'Choose a cost threshold between 0 and 32767.', 1;
DECLARE @advanced_options_was_on bit =
(
SELECT CONVERT(bit, value_in_use)
FROM sys.configurations
WHERE name = 'show advanced options'
);
IF @advanced_options_was_on = 0
BEGIN
EXEC sys.sp_configure 'show advanced options', 1;
RECONFIGURE;
END;
EXEC sys.sp_configure 'cost threshold for parallelism', @target_cost_threshold;
RECONFIGURE;
IF @advanced_options_was_on = 0
BEGIN
EXEC sys.sp_configure 'show advanced options', 0;
RECONFIGURE;
END;
The script is deliberately safe by default and uses 20 only as an example. Raise the threshold in small, reviewed increments, observe a complete business cycle, and compare Query Store evidence before and after. Set MAXDOP deliberately as well, based on SQL Server version, available logical processors, NUMA layout, and workload behavior instead of assuming that 0 is suitable.
Neither setting needs a restart, but both can change plan selection across the instance. Treat them as measured workload changes, not harmless checkboxes.
SELECT file_id,
name,
type_desc,
size / 128.0 AS size_mb,
CAST(CASE WHEN is_percent_growth = 1
THEN growth
ELSE growth * 8.0 / 1024
END AS decimal(18,2)) AS growth_value,
CASE WHEN is_percent_growth = 1
THEN 'PERCENT' ELSE 'MB'
END AS growth_unit
FROM tempdb.sys.database_files;
tempdb has accumulated enough folklore to qualify for its own mythology. Start with evidence. Look for data files sized for the workload, equal sizes and growth increments across the data files, and fixed-megabyte growth rather than percentage growth. Multiple equally sized data files are a standard starting point for allocation contention, commonly one per logical processor up to eight, but do not multiply files when the waits do not support that diagnosis.

To confirm allocation contention instead of diagnosing by tradition, look at what is waiting right now:
SELECT session_id,
wait_type,
wait_duration_ms,
blocking_session_id,
resource_description
FROM sys.dm_os_waiting_tasks
WHERE wait_type LIKE 'PAGELATCH[_]%'
AND resource_description LIKE '2:%';
2:1:1, 2:1:2, and 2:1:3 are familiar first-page examples for PFS, GAM, and SGAM in the first tempdb data file. Allocation pages repeat later in every file, so those three page numbers are examples, not a complete detector. This DMV is also a point-in-time view. Sample it repeatedly and look for sustained PAGELATCH waits on tempdb allocation pages before changing the file count. One screenshot is a clue. A repeated pattern is evidence.
SELECT name,
is_auto_create_stats_on,
is_auto_update_stats_on,
is_auto_update_stats_async_on
FROM sys.databases
WHERE database_id > 4;
Auto create and auto update should almost always be on. The interesting setting is is_auto_update_stats_async_on. With it off, a query that triggers an automatic statistics update waits for the update before compilation continues. Turning it on avoids that synchronous wait, but the triggering query compiles with the existing statistics while the refresh runs in the background. That is a tradeoff, not a universal recommendation. On SQL Server 2022 and later, also evaluate ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY to reduce lock contention from the background update.

The settings query is instance-wide. The next query is database-scoped, so run it inside each database that matters. It finds heavily modified statistics on large rowsets that deserve inspection:
SELECT OBJECT_SCHEMA_NAME(s.object_id) AS schema_name,
OBJECT_NAME(s.object_id) AS table_name,
s.name AS stats_name,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter
FROM sys.stats AS s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
WHERE sp.rows > 1000000
AND sp.modification_counter > sp.rows * 0.1
ORDER BY sp.modification_counter DESC;
The modification counter tracks changes to the leading statistics column. The 10 percent filter is a triage heuristic, not SQL Server’s internal automatic-update threshold. A large counter also does not prove that stale statistics caused the slow query. Compare rows_sampled with rows, then examine the affected query’s estimated and actual row counts. A targeted update with a higher sample rate can help when the statistics are genuinely responsible. Blanket full scans on every large table are how a maintenance job becomes the next incident report.
Most of the worst incidents I investigate trace back to a change that was technically correct and run at the wrong scale. The statement was valid. The table was enormous. SQL Server honored both facts.
You do not need a new platform to begin. You need a list, published before the change window, of the tables where nothing casual happens:

WITH table_footprint AS
(
SELECT object_id,
SUM(CASE WHEN index_id IN (0, 1)
THEN row_count ELSE 0 END) AS row_count,
SUM(reserved_page_count) * 8 / 1024.0 AS reserved_mb
FROM sys.dm_db_partition_stats
GROUP BY object_id
)
SELECT s.name AS schema_name,
t.name AS table_name,
f.row_count,
f.reserved_mb
FROM table_footprint AS f
JOIN sys.tables AS t
ON t.object_id = f.object_id
JOIN sys.schemas AS s
ON s.schema_id = t.schema_id
WHERE f.row_count > 50000000
ORDER BY f.row_count DESC;
The row count is approximate, and reserved_mb includes the table’s indexes. Replace 50 million with a threshold that reflects your environment. That is enough to create a deployment gate. The rule fits in one sentence: no potentially blocking or size-dependent DDL against anything on this list without a written execution plan, a tested rollback path, and estimates for duration, transaction-log growth, and lock impact. That one rule has prevented more outages for my clients than many far more impressive-looking projects.
Every shop has one. The month-end reporting workload arrives, flattens the transactional workload, gets investigated, gets explained, and then returns next month like a meeting nobody was brave enough to decline.
Governing that workload is supported where the edition permits. The values below are placeholders for a tested policy, not recommended production values. MAX_CPU_PERCENT is an opportunistic maximum that is enforced when CPU is contested, while CAP_CPU_PERCENT is a hard CPU ceiling. For ordinary disk-based workloads, MAX_MEMORY_PERCENT governs query workspace memory for the pool, not SQL Server’s total memory or buffer pool. Memory-optimized tables have additional pool behavior that must be evaluated separately.

USE master;
GO
CREATE RESOURCE POOL ReportingPool
WITH (MAX_CPU_PERCENT = 25,
CAP_CPU_PERCENT = 40,
MAX_MEMORY_PERCENT = 25);
CREATE WORKLOAD GROUP ReportingGroup
USING ReportingPool;
GO
CREATE FUNCTION dbo.fn_ClassifyWorkload()
RETURNS SYSNAME
WITH SCHEMABINDING
AS
BEGIN
RETURN CASE
WHEN SUSER_SNAME() = N'DOMAIN\ReportingService'
THEN N'ReportingGroup'
ELSE N'default'
END;
END;
GO
ALTER RESOURCE GOVERNOR
WITH (CLASSIFIER_FUNCTION = dbo.fn_ClassifyWorkload);
ALTER RESOURCE GOVERNOR RECONFIGURE;
This is an illustrative new configuration. If the server already has a classifier function, add the routing rule to that function instead of replacing it with this example. The classifier belongs in master and is evaluated for every new session, even when connection pooling is enabled. Reusing an existing pooled session does not create a new classification event, and existing sessions keep their current group. Keep the function simple, test with a genuinely new connection, verify the assigned workload group, and confirm dedicated administrator connection access before rollout. An overly restrictive pool can make a query run longer and hold locks longer, so validate the effect on the protected transactional workload as carefully as the effect on reporting.
Nearly everybody alerts on the outage. That is useful, but late. Far fewer teams alert on the condition that has been building for several minutes while the database is still answering calls and pretending everything is fine.

A current blocked request that has waited more than thirty seconds is one of the highest-value signals I know. Thirty seconds is an example threshold, not a law. Tune it to the workload, poll the query from a job, retain the results, and account for maintenance that is expected to block:
SELECT r.session_id,
r.blocking_session_id,
r.wait_time / 1000 AS wait_seconds,
r.wait_type,
r.wait_resource,
DB_NAME(r.database_id) AS database_name,
t.text AS running_sql
FROM sys.dm_exec_requests AS r
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.blocking_session_id <> 0
AND r.wait_time > 30000;
Positive blocking_session_id values identify another session. Negative values have special meanings, including orphaned distributed transactions and latch owners that SQL Server cannot identify. In particular, -5 by itself does not prove a performance problem. This query is an early-warning signal, not a complete blocking-chain analysis.
Add transaction-log utilization and the reason that log truncation is being held. This can expose pressure caused by an active transaction, missing log backups, replication, or an availability replica before the volume is full:
SELECT d.name AS database_name,
ls.total_log_size_mb,
ls.active_log_size_mb,
CAST(100.0 * ls.active_log_size_mb /
NULLIF(ls.total_log_size_mb, 0) AS decimal(6,2)) AS active_log_percent,
ls.log_since_last_log_backup_mb,
ls.log_truncation_holdup_reason
FROM sys.databases AS d
CROSS APPLY sys.dm_db_log_stats(d.database_id) AS ls
WHERE d.state_desc = 'ONLINE'
AND d.database_id > 4
ORDER BY active_log_percent DESC;
sys.dm_db_log_stats is available in SQL Server 2016 SP2 and later. On an availability-group secondary, the function returns only a subset of its normal columns, so missing size values should not be interpreted as zero pressure. The query shows current pressure and the truncation holdup, not the history of file-growth events. Capture autogrowth separately with Extended Events or your monitoring platform. The warning signal is often visible before the pager goes off, but only if you retain a baseline and alert on sustained abnormal values.

The first audit takes about an hour. Much of the remediation is configuration, code, and operating discipline using capabilities the organization already owns. Edition-specific features still need to be checked before anybody promises a change. So why is so much of the practical prevention layer unused?
Because prevention is invisible, and invisible work has no advocate.

The person who spends a quiet Thursday evaluating automatic plan correction, fixing a proven tempdb problem, testing the parallelism configuration, and writing a blocking alert has produced, from the outside, nothing at all. No incident report. No bridge call where they were heroic. Nothing dramatic to put in a review.
The person who fixes a catastrophic outage at four in the morning gets thanked in a company-wide email. The person whose preparation prevented the outage gets a quiet night and no email. I know which reward I prefer, but I also know which one organizations tend to notice.
Nobody here is behaving irrationally. The incentives point at the fire rather than at the wiring, and they have for a long time.
My practical advice is to describe prevention in the language of the incident it removes. Not “I enabled automatic tuning.” Instead, “eligible plan regressions can now be corrected automatically and verified after the force.” Not “I added an alert.” Instead, “we now detect sustained blocking while there is still time to act.” Same technical work, much clearer business value.
Two honest caveats belong here, and the second is the stronger one.
Prevention has a ceiling. You can remove known failure classes. You cannot remove the unprecedented. Monitoring and explanation will never be worth zero, and anyone promising a world with no incidents is selling you something.
You cannot prevent a failure class you have never understood. The root cause analysis is the input to the prevention work. My complaint was never that we write them. It is that we write them, file them, and do not do the next thing.
So the claim is narrower than the title suggests. Explanation is necessary. Treating explanation as the destination is the mistake.

At the end of every incident document I write, there is now a section that is not about the incident. It is about making the same failure class less welcome next time.
It names the failure class, states what would have made it impossible rather than merely visible, and gives a rough cost. Sometimes the cost is an afternoon and a checkbox. Sometimes it is a planned project. Both are easier to fund when the cost and the failure being removed are explicit.
Some clients skip that section. A few do not, and those are the clients I eventually stop hearing from. It is the strangest form of professional success I have encountered, and I have decided to enjoy it.
The thread underneath all of this is who holds the judgment when the tooling sounds confident, which is the argument across all thirty essays in my book AI: Nobody’s in There: But we’re still in here. All thirty are free to read at pinaldave.com. If you would rather hold a copy, it is on Amazon in paperback, Kindle and audiobook.
If you take one thing from this, take the smallest one. Record the version and edition, then run the Query Store state query from check 2 against your busiest database. It takes about two minutes. If the answer is READ_ONLY, the most valuable performance history on the server may already be disappearing quietly.
This is not a story about explaining incidents better, it is a story about making the explanation a rarer thing to need.
Reference: Pinal Dave (https://blog.sqlauthority.com/), SQL Server Prevention, X
First appeared on A Better Fire Alarm Is Still a Fire
What is a magic system? Learn 7 ways to create a spectacular magic system for your fantasy novel and make your world more believable.
In fantasy, magic makes your world go round. It is not an afterthought. It is an important literary device that shapes your writing.
Magic, and how it works, will dictate how your characters act. It does this the same way gravity makes the earth spin.
If you think this kind of thing is stupid, you are not going to be able to write about it. This is a deadly serious topic. It has made authors’ careers and it has relegated others to obscurity.
There are roughly two types of fantasy. High Fantasy and Low Fantasy.
There are roughly two magic systems. High Magic and Low Magic.
They are not related in any way. Not at all.
High Fantasy deals with dark lords and world ending events. Low Fantasy might deal with fixing the post office with the help of mythical creatures. (Would a centaur be a good postman?)
But, the the magic system affects the tone.
Writing Tip: When deciding the tone, decide how much magic you want in it, because having too much magic can make your story seem light-hearted.
Suggested reading: The 4 Pillars Of Fantasy
Your magic system must adhere to its own logic. If this breaks down, it creates plot holes. Plot holes need thousands of words to explain away and this will bore your reader.
Nobody wants to ask the question, ‘Why didn’t they just use magic to fly away from danger?’ when clearly you show your characters doing this as a pastime in another scene. Just say that it takes ten minutes to cast the ‘Fly’ spell. Then you can still have your dramatic chase scene through the woods.
The problem with consistency in high magic settings like Doctor Strange, the Marvel Comic, is that Doctor Strange can do anything including turning back time. So, you may forget what the rules are when you have so few of them. But whatever they are, don’t break them. Rather work around them.
In Harry Potter you need a wand to do magic. Simple. If you want to create tension and keep the story consistent, Harry just needs to drop his wand. Then he’s just a boy. Problem solved.
A simple rule to your magic system like this will save you trouble later.
Writing Tip: Create an internal logic that you find easy to understand and that is simple to write about. Then reference back to this whenever you write about magic.
Sometimes magic is what makes a character special. In children’s books, where you want a bland protagonist the kids can project themselves onto, magic should be restricted to the main cast only.
On the other hand, in some settings, it’s fun if everyone has some magic. Perhaps, cell phones are all made by two or three competing Wizards? Perhaps Samsung the Blue and Applesen the Greedy have a bet going to see who can make more muggles addicted to Instagram.
Magic worlds work in two ways.
One where everybody in the world knows it’s there.
OR
One where it’s a secret.
This will affect how people view your characters. In the Forgotten Realms, outside the Archmage Elminster’s tower, these warnings magically appear as a person walks towards it:
But in The Magicians, magicians hide from the public by often living double lives.
In the first setting, it clearly pays to advertise that a wizard lives here. In the second, it might be illegal in wizard society to even tell your extended family that magic is real.
Writing Tip: If in doubt, write magic as if it is a rare power. This automatically makes your readers see it as important, mystical, and interesting. It also allows them to engage with childlike wish fulfilment. This helps them buy into the setting.
Magic can come from three sources:
Note: A sorcerer is a source of magic.
Writing Tip: If you don’t have a story goal in mind finding out where magic comes from is a perfect way to give your characters something to strive for. Alternatively, use this as a big reveal that tells us something important about your world.
It you have magic and you don’t use it, what’s the point?
Have fun with it. Make fantastic creatures and enchanting landscapes. Tell us about how gnomes create black holes to suck up their trash. Or how an elf spent a thousand years talking to a tree, because he was just too polite to leave.
Have your character use it in ways that will either put wonder into your reader or at least make them laugh. Maybe even cry?
Writing Tip: People read about magic because it is entertaining. It has no real world value. So, if your magic is boring you are failing at a basic aspect of magic fantasy writing. If you are struggling to invent playful ways to use magic, ask a small child to explain how their phone works. Or even better your grandmother.
Don’t have magic just to have it.
Writing Tip: Sometimes, it is more interesting to have a reason to want magic than to be able to use magic. Your character’s motivations come before all other considerations. Magic is just a plot device to serve your story needs.
And you need to write something people want to read.
To really know how to make a magic system you need to steal from the best.
So, don’t start with them.
Writing Tip: I’ll paraphrase Terry Pratchett here. Writers are readers who have read so much that they start to overflow. Nobody has ever become worse at writing by reading more.
As I said at the beginning: magic makes your world go round. A great magic system can make your fantasy world feel real, but it needs to work with your story. Give your magic rules and limits, and think about how it affects your characters and their choices. Get this right, and your magic can add wonder, conflict, and plenty of possibilities to your novel.
[Top Tip: Learn how to write fantasy. Buy The Fantasy Workbook]

by Christopher Luke Dean (Secretly an alien from a long lost race of time travellers on earth to preserve the space-time continuum by writing listicals.) Christopher writes and facilitates for Writers Write.
Top Tip: Sign up for our free daily writing links.
The post What Is A Magic System? 7 Ways To Create A Spectacular Magic System For Your Novel appeared first on Writers Write.
Is AI code debt the new technical debt? As I keep learning, generating code and examining it – it’s worse.
In the old days, we worked hard to create technical debt. Just kidding, it was easy.
Technical debt has many definitions, and origin stories. But one thing is common: We leave the code as it is, knowing it could be better. The debt is the gap of effort of making it better.
And why the gap? Because we know we’ll see that code again. And when we do, it’ll be hard to change. Better code would have made it easier.
Now we’re in the age of genies. They can write any code. They can change any code. Do we need to worry about AI code debt?
Sure we do.
AI code debt is exactly the same as technical debt – we’re leaving the code as it is, knowing it could be better. The gap is still the effort of making it better.
But this time the gap is a lot bigger. There’s a lot more code, it’s probably a lot more complex, and some of it – let’s be frank – is code we didn’t review. So the gap is a lot bigger than we guess.
But that’s a bot problem, right? The code agent will deal with all the needed changes. We don’t need to even look at the code.
Nah, you know you will. You know what code is generated, and it’s not how you would have written it. And coding agents have the same problem of making sense of complex code bases as us.
And they’ll make mistakes.
Cleaning is not just “make it readable”. It’s preparing it for more changes, reducing dependencies and isolating interfaces – all in the context of future plans. And the agent doesn’t have this context.
In fact, it will assume another context, and we get stuck with the code and the assumptions.
Same as always: find the code you know you’ll touch again, and make that code easier to change. The problem starts when you don’t know where that code is.
Because, you didn’t write it. And you didn’t review all of it. If any.
Putting a price on technical debt before was an exercise in imagination. Now it’s pure fantasy.
First know what code was generated. And for that you need to enforce smaller code generation.
If you don’t enforce it, you’ll have a lot more to review. And if that happens you won’t review it all. It’s a human thing.
Then, you can wish the genie to refactor it to take the shape you want. Genies are good at transformations. And if the genie breaks something, your tests will tell you.
You do have tests, right?
The old technical debt was based on maintenance work. Now we have bigger maintenance queued up, along with risks of unverified code. Not cool.
So, first, we need to be aware of the AI code debt – we’re creating code that will cost a lot more to maintain, than “regular” code.
Before, we thought “it’s ok, we’ll take care of that later”, and then, when the bill came it was a lot more than we thought.
Now? Expect a much bigger one.
AI code debt is technical debt created by generated code: you leave the code as it is, knowing it could be better, and the debt is the effort of making it better. The difference is scale. There’s more of it, it’s more complex, and some of it was never reviewed.
Not in kind, only in size and visibility. The old debt was something you put there and remembered. This debt arrived while you were reading something else, so you don’t know where it is.
Not on its own. Cleaning up means preparing code for changes you plan to make, and the agent doesn’t have that context. It will assume a different one, and you’re left with the code and the assumptions.
With tests you wrote before the refactor. Directing the genie to reshape code is fine as long as something independent tells you when it breaks.
I write about this stuff every two weeks. What generated code actually costs, and what to do about it before the bill lands.
The post The New Technical Debt first appeared on TestinGil.Get caught up on the latest technology and startup news from the past week. Here are the most popular stories on GeekWire for the week of Aug. 2, 2026.
Sign up to receive these updates every Sunday in your inbox by subscribing to our GeekWire Weekly email newsletter.
Zillow Group laid off more than 500 employees Tuesday, about 7% of its workforce, one day before it reports second-quarter earnings. … Read More
Jeff Dean, who earned his computer science Ph.D. … Read More
Teri Hatfield, Salesforce EVP and Tableau CRO, has taken a role at Iterable; Gates Foundation’s legal director has left for Arnold & Porter; and Seattle-area startups AIM and Gravitics have added to their C-suites. … Read More
Google is laying off 52 employees in Washington state, according to a state filing. … Read More
Affected positions include software engineers, product management directors, incident commanders, technical support engineers, and leadership roles across marketing and sustainability. … Read More
Charles Lamanna, who leads Microsoft’s Copilot, agents, and platform work, is helping build the coming Copilot Super App — a single front door meant to unify the company’s sprawling AI products. … Read More
A WARN notice filed with Washington state shows Zillow Group is cutting 91 jobs in its home state, a fraction of the more than 500 layoffs announced this week. … Read More
Jyoti Shukla is taking the product strategy skills she developed at Microsoft, Starbucks, Nordstrom and SiriusXM and applying them to a station she has relied on as a fan for decades. … Read More
After nearly seven years at the helm of the Paul G. … Read More
Wild Zebra raised $6 million to expand an AI learning platform for math and reading that asks students questions rather than just giving them answers. … Read More