Skip to main content

Fix Azure AI Search custom skill timeouts that silently drop documents from RAG indexes, causing missing data, indexing failures, and inaccurate retrieval results.

Pipeline FixAzure AI SearchCustom SkillsRAGSilent Data Loss

The "Missing Data" RAG Pipeline Bug: Fixing the 30-Second Timeout in Azure AI Search Custom Skills

Your RAG app confidently tells a user it has no information on a document you personally uploaded last week. The indexer says Success. The blob is right there in storage. What actually happened is that a custom skill blew a timeout, the indexer downgraded the failure to a warning, and the document was silently dropped from the index — and it will keep happening on every run until you find it.

The exact error this guide resolves
Could not execute skill because it did not execute within the time limit '00:00:30'.
This is likely transient. Please try again later. For custom skills, consider
increasing the 'timeout' parameter on your skill in the skillset.

# It appears as a WARNING — not an error — in the indexer execution result:
GET /indexers/my-indexer/status?api-version=2024-07-01

{
  "lastResult": {
    "status": "success",            <-- the indexer says it SUCCEEDED
    "itemsProcessed": 487,
    "itemsFailed": 13,              <-- 13 documents silently never made it
    "warnings": [
      {
        "key": "https://sa.blob.core.windows.net/docs/contract-2024-Q3.pdf",
        "name": "myCustomSkill",
        "message": "Could not execute skill because it did not execute within
                    the time limit '00:00:30'."
      }
    ]
  }
}

# Downstream, this is all your users ever see:
> "I don't have any information about the Q3 contract."

Symptom: Documents you know exist are unretrievable in RAG, while the indexer reports success.  Failure point: Indexer → skillset → Custom Web API skill exceeds its timeout → the enrichment fails → the document is skipped.  Default platform behaviour: The indexing pipeline is synchronous. If your skill does not respond inside the timeout window, the call is abandoned. And because maxFailedItems is commonly set to a non-zero value, the failure is downgraded to a warning and indexing continues — producing a green status over an index with holes in it.

PT30S
The default custom-skill timeout — not a hard limit. Most teams never set it, and never realise it is adjustable
PT230S
The genuine ceiling. You may raise the timeout to a maximum of 230 seconds — and not one second beyond
Synchronous
The indexing pipeline blocks on your skill. There is no async callback, no polling — respond in time or the document is lost
"success"
What the indexer reports while silently discarding documents, if maxFailedItems is non-zero

The cruelty of this bug is that every dashboard lies to you. The blob is in storage. The indexer status says success. The skillset is valid. The search index exists and returns results for other queries. Nothing anywhere is red. And yet a specific set of documents — invariably the largest and most important ones, the hundred-page contracts and the scanned reports — are simply not in the index, and your RAG application answers questions about them by confidently claiming ignorance. The failure is real, it is logged, and it is sitting in a warnings array that nobody reads because the status field above it says the run succeeded.

Figure 1 — How a 30-second timeout becomes a permanently missing document
THE PATH A LARGE DOCUMENT TAKES — AND WHERE IT VANISHESBLOB STORAGE300-page PDF✓ presentINDEXERcracks the doc✓ okCUSTOM WEB API SKILLwhole doc in ONE call⏱ 42s > 30s TIMEOUTSYNCHRONOUS — call abandonedmaxFailedItemsis it 0, or > 0 ?THE FORK= 0 → INDEXER FAILSLoud. You find out. Good.> 0 → WARNING ONLYDoc skipped. Status: success.THE SILENT PATH — WHAT THE USER ACTUALLY EXPERIENCESSEARCH INDEXdocument ABSENTVECTOR SEARCHreturns 0 chunksLLM CONTEXTempty for this doc"I don't have information on that."The bug reaches the user as an ANSWER.Nothing in this chain is red. The blob exists, the indexer says "success", the index is queryable, the LLM answers fluently.The only trace is a warning object nobody reads. This is why the bug survives for months — and why Fix 4(making it fail loudly) matters as much as the timeout fix itself.
A skill timeout does not stop the pipeline — it removes one document from it. With a non-zero maxFailedItems, that removal is recorded as a warning while the run reports success, so the document is absent from the index, absent from retrieval, and absent from the LLM's context. The user experiences the bug as a confident "I don't know."
01The Premise Is Wrong: 30 Seconds Is a Default, Not a LimitCorrection

Before you architect a workaround, know this: the 30-second cap is not fixed, and it is not rigid. A very large number of teams have built elaborate queue-and-callback machinery to escape a limit that they could have raised with a single JSON property.

Microsoft's documentation is unambiguous. By default, the connection to a custom skill endpoint times out if no response is returned within a 30-second window (PT30S). But you can increase that interval to a maximum of 230 seconds (PT230S) by setting the timeout parameter on the skill definition. Most skillsets simply never set it, inherit the default, and their authors conclude the platform is inflexible.

The one-property fix most teams never apply{ "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill", "description": "This skill has a 230-second timeout", "uri": "https://my-func.azurewebsites.net/api/extract", "timeout": "PT230S", // <-- the default is PT30S. This is legal. "batchSize": 1, "context": "/document", "inputs": [ { "name": "input", "source": "/document/content" } ], "outputs": [ { "name": "output", "targetName": "extracted" } ] }

So the honest framing of the problem is this: there is a hard limit, but it is 230 seconds, not 30. That distinction matters enormously, because it changes what kind of problem you are solving.

Your skill's real execution timeWhat you actually haveThe correct fix
Under 30sNo problem — you are hitting a different bugLook elsewhere (auth, response shape, cardinality)
30s – 230sA configuration problemSet timeout. Takes five minutes. Do this first
Over 230sAn architectural problemReduce the work per invocation — chunk before the skill (Section 6)
230 seconds is a genuine wall

Do not go looking for a way past it. 230 seconds is the maximum permitted value for the timeout parameter, and the indexing pipeline is synchronous — the indexer blocks on your HTTP response. There is no async callback, no polling contract, no "come back later" status. If your skill cannot answer within 230 seconds, the answer is not a bigger timeout. It is less work per call. That is what the rest of this guide is about.

02Why a Timeout Becomes Silent Data LossRoot Cause

A timeout, on its own, is a manageable failure. What turns it into the missing data bug — a bug that survives in production for months — is the indexer's error tolerance setting.

maxFailedItems tells the indexer how many documents may fail before the whole run is declared a failure. It is very commonly set to a non-zero value (often -1, meaning "unlimited"), because on a large corpus you do not want one corrupt PDF to abort a six-hour indexing run. That is a reasonable instinct. It also means that every skill timeout is downgraded from an error into a warning, the affected document is skipped, and the run reports "status": "success".

maxFailedItemsWhat happens on a skill timeoutDo you find out?
0The indexer run fails immediatelyYes — loudly, in status and alerts
> 0 (e.g. 10)Document skipped; counted toward the budget; run continuesOnly if you read the warnings array
-1 (unlimited)Every failing document is silently skipped, foreverNo. This is the missing-data bug

The consequence is a search index that is quietly incomplete. And the documents it drops are not random — they are systematically the largest and most content-rich ones, precisely because size is what causes the timeout. Your RAG system ends up missing exactly the documents your users most want to ask about: the long contracts, the detailed reports, the scanned archives.

The bug is self-selecting for importance

This is the part worth sitting with. A random data-loss bug would be annoying. This one is biased: it discards documents in direct proportion to how much content they contain. The 400-page master services agreement times out; the two-page memo sails through. So your index is fullest exactly where the content is thinnest, and emptiest exactly where it matters most — and the failure presents as your AI being uselessly ignorant about your most important material.

03Skill Cardinality: The Thing Nobody ChecksDiagnosis

Before tuning anything, you must know how many times your skill is actually invoked per document — because it determines everything about how you fix this, and it is set by one easily-overlooked property: context.

context valueInvocation cardinalityPayload per call
/documentOnce per documentThe entire document. A 300-page PDF arrives in a single call — this is what blows the timeout
/document/contentOnce per documentWhole content field — same problem
/document/pages/*Once per page/chunkOne chunk. Small, fast, predictable — this is the fix

Microsoft's own scaling guidance makes this the first thing to establish: know whether the skill executes once per document or multiple times per document, because if it runs many times per document you should stay on the lower side of batchSize and degreeOfParallelism to reduce churn.

This is the whole game

If your custom skill's context is /document or /document/content, you are handing your entire 300-page document to one HTTP request and asking it to finish in under 30 seconds. No timeout value fixes that at scale — it just moves the wall from 30s to 230s and buys you slightly bigger documents before the same failure returns. The real fix is to change the cardinality, so the skill is invoked many times with small payloads instead of once with an enormous one. That is Section 6.

Architectural Topology: Failing vs Remediated

ComponentFailing configuration (current)Remediated configuration (fix)
Skill timeoutUnset — silently inherits the PT30S defaultExplicitly set, sized to the real p99 of the skill
Skill context/document — the whole document in one call/document/pages/* — one small chunk per call
ChunkingInside the skill, or not at allBefore the skill, via SplitSkill
Payload per invocationUnbounded — grows with document sizeBounded and predictable, regardless of document size
batchSizeDefault — many docs per request, compounding the timeoutTuned; lowered when cardinality is high
maxFailedItems-1 — every failure silently swallowed0 in CI; a small, alerted budget in production
Failure visibilityWarnings array nobody reads; status says successAlert on itemsFailed > 0 and on warning count
Long-running workExecuted inline, inside the synchronous skill callDispatched — the skill returns fast; work happens elsewhere
05Fix 1 — Raise the Timeout (the 5-Minute Fix That Isn't the Real Fix)Quick Win

Do this first, because it costs five minutes and it may be all you need. But be clear-eyed about what it is: headroom, not a solution. Raising the timeout does not make your skill faster; it just lets it be slow for longer before the same wall arrives.

REST — PUT the updated skillset with an explicit timeoutPUT https://[service].search.windows.net/skillsets/my-skillset?api-version=2024-07-01 Content-Type: application/json api-key: [admin key] { "name": "my-skillset", "skills": [ { "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill", "name": "extractSkill", "uri": "https://my-func.azurewebsites.net/api/extract", // Default is PT30S. Max permitted is PT230S. Set it deliberately. "timeout": "PT230S", // Fewer documents per request = less work to finish inside the window. "batchSize": 1, "context": "/document", "inputs": [ { "name": "input", "source": "/document/content" } ], "outputs": [ { "name": "output", "targetName": "extracted" } ] } ] }

Note the pairing of timeout and batchSize. Microsoft's guidance is explicit: if your custom skill cannot execute consistently within 230 seconds, reduce the batchSize so it has fewer documents to process in a single execution. These two knobs work together — a large timeout with a large batch size just means you fail slower.

Size the timeout from data, not superstition

Do not simply set PT230S everywhere and move on. An over-long timeout is its own problem: it holds indexer threads open, slows the whole pipeline, and masks a skill that is quietly degrading. Measure your skill's real p99 execution time under production-shaped payloads, then set the timeout to that plus a sensible margin. If the honest answer to "what is my p99?" is "more than 230 seconds," you have an architecture problem and no amount of configuration will save you — go to Section 6.

06Fix 2 — Chunk Before the Skill (the Real Fix)Architecture

This is the fix that actually solves the problem, and it is a change in where the chunking happens rather than whether it happens. The instinct is to make the skill handle oversized payloads — to add chunking logic, streaming, or batching inside the custom skill. That is the wrong layer. The skill is downstream of the problem; by the time the payload arrives, the timeout clock is already running.

Instead, put a SplitSkill in front of your custom skill, and then set your custom skill's context to iterate over the resulting chunks. This inverts the economics completely: instead of one call that must process 300 pages in 30 seconds, you get 300 calls that each process one page in under a second.

Skillset — SplitSkill first, then a per-chunk custom skill{ "name": "chunked-skillset", "skills": [ // STEP 1: Split the document into bounded chunks BEFORE any custom work. { "@odata.type": "#Microsoft.Skills.Text.SplitSkill", "name": "splitSkill", "textSplitMode": "pages", "maximumPageLength": 5000, // bounded payload, by construction "pageOverlapLength": 500, // preserve context across boundaries "context": "/document", "inputs": [ { "name": "text", "source": "/document/content" } ], "outputs": [ { "name": "textItems", "targetName": "pages" } ] }, // STEP 2: The custom skill now runs ONCE PER CHUNK, not once per document. // The '/*' is the entire fix — it changes the invocation cardinality. { "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill", "name": "extractSkill", "uri": "https://my-func.azurewebsites.net/api/extract", "context": "/document/pages/*", // <-- ONE INVOCATION PER CHUNK "timeout": "PT60S", // now generous, not desperate "batchSize": 5, // low, because cardinality is high "degreeOfParallelism": 3, "inputs": [ { "name": "text", "source": "/document/pages/*" } ], "outputs": [ { "name": "result", "targetName": "extractedChunk" } ] } ] }

The document size is now decoupled from the skill's execution time. A two-page memo produces two calls; a 300-page contract produces 300 calls. Each individual call is small, fast, and nowhere near any timeout. Crucially, the skill's execution time no longer grows with document size — which is what made the old design fail on exactly your most important documents.

Why chunking upstream also fixes your RAG quality

There is a bonus here that has nothing to do with timeouts. RAG needs chunked content anyway — you cannot embed a 300-page document as a single vector and expect useful retrieval. So chunking before the skill is not a workaround you are grudgingly adopting to dodge a limit; it is the shape a RAG pipeline should have had in the first place. The timeout was the symptom that forced you to discover the correct architecture. Set pageOverlapLength so chunks overlap slightly, or you will sever sentences at chunk boundaries and degrade retrieval at exactly those seams.

Mind the output shape when you change cardinality

Changing context to /document/pages/* changes the shape of your enrichment tree — your skill's output is now an array of per-chunk results, not a single value. Your output field mappings must be updated accordingly, and if you are projecting to a knowledge store or an index with one-document-per-chunk, your index schema likely needs to change too. This is the part of the migration that bites people who copy the skillset change without following through. Test on a single document before you reset the indexer.

Figure 2 — The cardinality inversion: one enormous call vs many small ones
✗ context: "/document" — the whole document in ONE invocation300-PAGE PDF~1.4 MB textone payloadone HTTP callCUSTOM SKILLmust finish ALL 300 pageswithin ONE timeout windowexec time ∝ document size⏱ TIMEOUT42s > 30s limitDOC DROPPEDRaising timeout to 230sonly moves the wall.A 900-page doc still dies.✓ SplitSkill → context: "/document/pages/*" — one invocation PER CHUNK300-PAGE PDFsame documentunchangedSplitSkillmaxPageLength= 5000 charschunk 1chunk 2chunk 3… × 300CUSTOM SKILL × 300each call: 1 chunk, <1sexec time INDEPENDENTof document size✓ ALL 300 INDEXEDNo timeout possible.Scales to any size.THE KEY INSIGHT: the timeout is not the bug. UNBOUNDED WORK PER INVOCATION is the bug.Raising PT30S → PT230S makes the failure rarer without making it impossible — the wall just moves further out.Bounding the payload with SplitSkill removes the failure mode entirely, at any document size, forever.
Changing the skill's context from /document to /document/pages/* is the entire architectural fix. It converts execution time from a function of document size into a constant per chunk — which means the pipeline no longer has a maximum document size at which it silently begins losing data.
07Fix 3 — Tune batchSize and degreeOfParallelismTuning

Once cardinality is right, throughput becomes a tuning exercise across three interacting knobs. There is no one-size-fits-all recommendation — Microsoft says so explicitly, and you should plan to test configurations rather than copy someone else's numbers.

KnobWhere it livesWhat it controls
batchSize (skill)Skill definitionHow many records are packed into one HTTP request to your skill
degreeOfParallelismSkill definitionHow many concurrent requests the indexer makes to your skill
batchSize (indexer)Indexer definitionHow many documents are read from the data source and enriched concurrently

The strategy divides into two shapes: fewer large requests, or many small requests. Which one you want depends on your skill. The critical rule, straight from Microsoft's scaling guidance: if your skill executes multiple times per document — which, after Fix 2, yours now does — stay on the lower side of batchSize and degreeOfParallelism to reduce churn, and consider raising the indexer batch size for more scale instead.

Indexer definition — coordinate the two batch sizes{ "name": "my-indexer", "dataSourceName": "my-blob-ds", "skillsetName": "chunked-skillset", "targetIndexName": "my-index", "parameters": { "batchSize": 10, // docs read + enriched concurrently "maxFailedItems": 0, // see Fix 4 — do NOT hide failures "maxFailedItemsPerBatch": 0, "configuration": { "dataToExtract": "contentAndMetadata", "parsingMode": "default" } } }
Do not create a bottleneck between the two batch sizes

The skill's batchSize and the indexer's batchSize must be coordinated, not tuned in isolation. A common failure is an indexer pulling documents far faster than the skill can drain them, which builds a queue, inflates per-call latency under load, and reintroduces the very timeouts you just eliminated — this time only under production traffic, which makes it look intermittent and "transient." If the errors only appear at scale, this is where to look.

08Fix 4 — Fail Loudly: Stop Hiding Dropped DocumentsCritical

Every fix above reduces the chance of a timeout. This one ensures that if a timeout ever happens again, you find out — instead of discovering it months later through a user complaint. It is the difference between a bug and a silent, permanent data-integrity defect.

Indexer — make failures fail{ "parameters": { // In CI and staging: ZERO tolerance. Any dropped doc fails the run. "maxFailedItems": 0, "maxFailedItemsPerBatch": 0 } } // In production, a small non-zero budget is defensible — one genuinely // corrupt PDF should not abort a six-hour run. But it must be SMALL, // and it MUST be alerted on. "-1" is never an acceptable production value.
Read the warnings the status field is hiding from youGET https://[service].search.windows.net/indexers/my-indexer/status?api-version=2024-07-01 api-key: [admin key] # Do NOT stop at lastResult.status. Read these three fields, every run: # lastResult.itemsProcessed how many made it # lastResult.itemsFailed how many did NOT <-- the missing data # lastResult.warnings[] WHY, and for which document key
KQL — alert on silent document loss// Requires diagnostic settings on the search service -> Log Analytics. // This is the alert that would have caught the bug on day one. AzureDiagnostics | where ResourceProvider == "MICROSOFT.SEARCH" | where OperationName == "Indexers.Status" | extend failed = toint(customDimensions["itemsFailed"]) | where failed > 0 | project TimeGenerated, Resource, failed, warning = tostring(customDimensions["errorMessage"]) | order by TimeGenerated desc // Alert rule: itemsFailed > 0 over any 1h window -> page someone. // A "successful" run that dropped documents is NOT a success.
Reconcile counts — the only test that cannot lie

The most reliable detection is also the simplest: compare the number of source documents to the number of indexed documents. If blob storage has 500 documents and your index has 487, you have lost 13 — regardless of what any status field claims. Run this reconciliation on a schedule and alert on any drift. It catches not just skill timeouts but every other silent-drop failure mode in the pipeline, including ones nobody has thought of yet.

09Fix 5 — When 230 Seconds Genuinely Isn't EnoughEscape Hatch

Occasionally the work per irreducible unit genuinely exceeds 230 seconds — a heavyweight ML model, an OCR pass over a huge scanned image, an external API that is simply slow. You cannot chunk your way below the wall, and the synchronous pipeline will not wait. In that case, stop trying to do the work inside the skill at all.

The pattern is to make the custom skill a fast dispatcher rather than a worker. It does not perform the expensive computation — it looks up an already-computed result, and the expensive work happens outside the indexing pipeline entirely.

Python — the skill becomes a fast lookup, not a slow workerimport azure.functions as func # The custom skill endpoint. MUST return well inside the timeout. # It does NO heavy work. It only reads a precomputed result. def main(req: func.HttpRequest) -> func.HttpResponse: body = req.get_json() values = [] for record in body["values"]: doc_id = record["data"]["docId"] # Look up the result computed EARLIER, out of band. result = results_store.get(doc_id) # fast: blob / Cosmos / cache if result is None: # Not ready yet. Return a WARNING, not an error — the indexer # continues, and the next scheduled run will pick it up once # the out-of-band worker has finished. This is intentional. values.append({ "recordId": record["recordId"], "data": {}, "warnings": [{"message": f"Enrichment pending for {doc_id}"}], }) else: values.append({ "recordId": record["recordId"], "data": {"extracted": result}, }) return func.HttpResponse( json.dumps({"values": values}), mimetype="application/json" )

The heavy processing runs as a separate pipeline — a Durable Function, a Container Apps job, a Service Bus consumer — triggered when the document lands in storage, long before the indexer ever sees it. By the time the indexer runs, the answer is already sitting in the store waiting to be read.

This is the "rewrite the data injection layer" fix, done properly

Notice what has happened architecturally: the expensive work has moved upstream of the indexer, into the ingestion path. That is the correct home for it. The indexing pipeline is a synchronous enrichment system with a hard 230-second budget per call — it is not, and was never intended to be, a general-purpose batch compute platform. If you are fighting its timeouts, you are usually asking it to be one. Move the heavy lifting to ingestion, pre-chunk and pre-compute there, and let the skillset do what it is good at: fast, bounded enrichment.

Validation & Verification: Confirm the Fix

Because the failure is silent, "the indexer says success" proves nothing — it said that while the bug was live. Validation must prove a document count reconciliation, not a status code. Three steps.

Step 1 — Test the skill directly against your worst payload# Take your LARGEST real document — the one that was being dropped — and # POST a single chunk to the skill, in the exact custom-skill format. curl -X POST "https://my-func.azurewebsites.net/api/extract" \ -H "Content-Type: application/json" \ -w "\n\nHTTP %{http_code} — total time: %{time_total}s\n" \ -d '{ "values": [ { "recordId": "worst-case-1", "data": { "text": "<one 5000-char chunk>" } } ] }' # PASS: HTTP 200, and total time comfortably under your configured timeout. # The response MUST match the custom skill contract — a "values" array with # matching recordId. An invalid shape produces "Web Api skill response is # invalid" and drops the document just as surely as a timeout does.
Step 2 — THE test that matters: reconcile source count vs indexed count# Count the documents in the source. az storage blob list --container-name docs --account-name mysa \ --query "length(@)" -o tsv # -> 500 # Count the DISTINCT parent documents in the index. # (If you chunked, count distinct parent keys — not chunks!) curl -s -X POST \ "https://[svc].search.windows.net/indexes/my-index/docs/search?api-version=2024-07-01" \ -H "api-key: [key]" -H "Content-Type: application/json" \ -d '{ "search": "*", "facets": ["parentId,count:10000"], "top": 0 }' \ | jq '.["@search.facets"].parentId | length' # -> 500 ✓ PASS (before the fix this returned 487 — the 13 lost docs) # PASS: source count == indexed distinct parent count. No silent loss. # FAIL: any gap. The gap IS your missing data. Go read the warnings.
Step 3 — Negative test: prove the pipeline now fails LOUDLY# Deliberately break the skill (e.g. add a sleep longer than the timeout), # run the indexer, and confirm the run FAILS instead of quietly succeeding. GET /indexers/my-indexer/status?api-version=2024-07-01 # PASS (with maxFailedItems: 0): # "status": "transientFailure" — the run FAILED. You will be alerted. # "itemsFailed": 1 # # FAIL: # "status": "success" with "itemsFailed": 1 # -> maxFailedItems is STILL swallowing your data loss. Set it to 0. # Then query the index for the specific document you know should be there: curl -s "https://[svc].search.windows.net/indexes/my-index/docs?api-version=2024-07-01&search=Q3+contract&\$top=1" \ -H "api-key: [key]" | jq '.value | length' # -> must be >= 1. If it is 0, the document is still missing.
What "fixed" actually means here

Three conditions, all required. One: the source document count equals the indexed document count — this is the only claim that cannot be faked by a green status field. Two: the skill's execution time is now independent of document size, so the failure cannot silently return as your corpus grows. Three: a future timeout would fail the run rather than emit an unread warning. If you fix the timeout but leave maxFailedItems: -1 in place, you have not fixed the missing-data bug — you have only made it rarer, and you will lose the next document just as quietly as you lost the last one.

Key Takeaways

30 seconds is a default, not a fixed limit. Set "timeout": "PT230S" on the skill. Many teams build elaborate workarounds for a wall they could have moved with one JSON property.
But 230 seconds is a real wall. The indexing pipeline is synchronous — no callbacks, no polling. Above 230s, no configuration saves you; only less work per call does.
The timeout isn't the bug — unbounded work per invocation is. A skill with context: "/document" takes time proportional to document size. Raising the timeout just moves the wall.
Chunk before the skill, not inside it. SplitSkill first, then context: "/document/pages/*". Execution time becomes constant per chunk and independent of document size — permanently.
maxFailedItems: -1 is what turns a timeout into silent data loss. It downgrades dropped documents to unread warnings under a green "success" status. This is the actual missing-data bug.
The bug self-selects for your most important documents. It drops content in proportion to how much of it there is — so your index is emptiest exactly where it matters most.
Validate by reconciling counts, not by reading status. Source document count must equal indexed document count. Everything else can lie to you; that number cannot.

Frequently Asked Questions

Is the 30-second custom skill timeout in Azure AI Search actually fixed?
No — this is the most common misconception about this error, and it sends people down the wrong path. Thirty seconds (PT30S) is the default timeout applied when you don't specify one. You can raise it to a maximum of 230 seconds (PT230S) by setting the timeout parameter in the skill definition, and that takes about five minutes. What is genuinely fixed is the 230-second ceiling: the indexing pipeline is synchronous, so the indexer blocks on your HTTP response, and there is no async callback or polling contract available. If your skill can't respond within 230 seconds, no configuration will help — you need to reduce the work per invocation instead.
Why does my indexer say "success" when documents are missing from the index?
Because of maxFailedItems. This parameter tells the indexer how many documents may fail before the entire run is declared a failure, and it's very commonly set to a non-zero value — often -1 for unlimited — so that one corrupt file doesn't abort a long indexing run. The side effect is that a skill timeout gets downgraded from an error into a warning: the document is skipped, the warning is recorded in the warnings array, and the run reports "status": "success". Check itemsFailed and the warnings array, not just the status. Better still, reconcile your source document count against your indexed document count — that number can't be misleading.
Should I add chunking logic inside my custom skill?
No — that's the wrong layer, and it's the instinct this whole article is arguing against. By the time your skill receives the payload, the timeout clock is already running, so chunking inside the skill means you're racing the same deadline with extra work to do. Put a SplitSkill in front of your custom skill and set the custom skill's context to /document/pages/*, so the indexer invokes it once per chunk instead of once per document. This converts execution time from a function of document size into a constant per chunk. It also happens to be the shape your RAG pipeline needs anyway, since you can't usefully embed a 300-page document as a single vector.
What's the difference between skill batchSize and indexer batchSize?
The skill's batchSize controls how many records are packed into a single HTTP request to your custom skill endpoint. The indexer's batchSize controls how many documents are read from the data source and enriched concurrently. They must be coordinated, because tuning one in isolation creates bottlenecks — an indexer pulling documents faster than the skill can drain them builds a queue, inflates latency under load, and reintroduces timeouts that only appear at production scale and therefore look "transient." Microsoft's guidance is that if your skill executes multiple times per document (which it will after you fix cardinality), you should stay on the lower side of both batchSize and degreeOfParallelism to reduce churn, and reach for the indexer batch size when you need more scale.

Popular posts from this blog

Learn how to use Azure Chaos Studio to simulate data center outages, test Azure OpenAI failover, and validate AI app resiliency using KQL and CLI workflows

Resiliency Testing Chaos Studio Zone Down Azure OpenAI Failover Testing AI Resiliency: Using Azure Chaos Studio to Simulate Data Center Outages on Your LLM Every multi-region Azure OpenAI architecture diagram has a failover arrow drawn on it. Almost none of them have ever actually been triggered. The arrow is a hypothesis, confirmed only by a real outage — unless you deliberately cause a controlled one first, on your own schedule, with a rollback plan, instead of finding out during an incident that the failover you designed never quite worked the way the diagram promised. The failure signature this guide resolves # The gap this article closes — a real architecture review finding: Design doc, page 4: "In the event of a regional outage, Azure Front Door automatically routes traffic to the secondary Azure OpenAI deployment in West Europe, with an expected failover time under 60 seconds." Verification performed to support this claim: NONE. Last time this path was ac...

Improve AI application performance by reducing latency, optimizing embeddings, and lowering cloud inference costs

Performance Fix Foundry Local 1.2 Linux ARM64 Embeddings Offline ASR The Edge Latency Drop: Fixing Latency Spikes by Offloading Embeddings to Foundry Local 1.2 You are paying a full cloud round trip — network, TLS, queue, throttle risk — to turn a twelve-word search query into a vector. That is the most expensive way possible to do one of the cheapest computations in your stack. Foundry Local 1.2 now runs on Linux ARM64, which means embeddings and speech recognition can happen on a Raspberry Pi, a Jetson, or a Graviton instance — offline, unmetered, and in single-digit milliseconds. The failure signature this guide resolves # Application Insights — the embedding call, not the LLM, is your tail latency: name p50 p95 p99 calls/day POST /embeddings (cloud) 89 ms 412 ms 3,847 ms 1,240,000 POST /chat/completions (cloud) 940 ms 1,720 ms 2,910 ms 38,000 ^^^^^^^^ ...

Learn how to select Azure Files and Blob storage tiers, avoid early deletion fees, model costs, and automate lifecycle management for large file migrations.

Choosing the Right Azure Storage Tier for Large File Migrations The complete decision framework for storage tier selection during large file migrations — Azure Files tiers, Blob tiers, cost modelling, early deletion traps, lifecycle automation, and the 2026 changes that affect every migration running today. By Francis Avorgbedor | Azure Engineer  ·  July 14, 2026  ·  18 min read  ·  Storage Tiers · Cost Optimisation · Migration FA Francis Avorgbedor Azure Engineer  ·  SEVENAI  ·  Azure Field Notes 9 Distinct Azure storage tiers across Files and Blob — most engineers know only 3 15hrs Archive tier rehydration time at standard priority — the delay teams forget to plan for 128KB Minimum billable object size for Cool/Cold/Archive from July 2026 — a 32× trap for small files 70% How much retrieval and transaction fees add to a theoretical Archive storage bill The most expensive mistake I see on large Azure file migrations is not choosing the w...

Find the hidden Windows 10 system files consuming up to 500GB, including hibernation, shadow copies, backups, and WinSxS, with safe cleanup steps.

  The 500GB System File That Eats Your Hard Drive Something on your Windows 10 drive is consuming hundreds of gigabytes and the normal tools cannot find it. This guide identifies every known culprit — from hibernation files and shadow copies to runaway backups and the Windows component store — and tells you exactly what is safe to delete, what to leave alone, and what the commands actually do.

Learn safe methods to reset Azure virtual machines using managed disks while preserving critical workloads

How to Reset an Azure Virtual Machine to Factory Settings Using a Managed Disk Azure does not have a single "factory reset" button. What it does have is something better: the OS Disk Swap — a method that swaps out the corrupted or misconfigured OS disk for a clean Windows Server managed disk without deleting the VM, its NICs, its IP addresses, or any attached data disks. Here is how it works, when to use it, and the exact steps to execute it safely. FA Francis Avorgbedor Azure Engineer July 16, 2026 15 min read Azure VMs · Windows Server · Real-World Fix 3 Methods to achieve a clean Windows Server installation on an existing Azure VM ~15min Typical OS Disk Swap duration — VM retains its NICs, IPs, and data disks throughout 0 Data disks affected by an OS Disk Swap — data disks remain attached and untouched 1 Snapshot of the original OS disk you must take before starting — no exceptions Introduction Why Azure Does Not Have a Simple Factory Reset — and What to Do Instead On a ph...

Determine Windows 11 compatibility, upgrade requirements, costs, and performance expectations on older hardware

Can I Update My Old Computer to Windows 11 — and How Much Will It Cost? Your i7, 16GB RAM, 512GB SSD machine is powerful enough to run Windows 11 comfortably. The TPM 2.0 and Secure Boot wall is a security checkbox, not a performance ceiling. Here are two proven ways to get past it, what each one costs, and what you are trading away by doing so. $0 Cost of the Windows 11 licence if your existing Windows 10 is genuine — the upgrade remains free in 2026 2 Proven methods to bypass TPM 2.0 and Secure Boot — Rufus (easy) and Registry edit (manual) 25H2 Current Windows 11 version — all known bypass methods tested and confirmed working as of July 2026 Oct 2025 Windows 10 end of life — no more security updates. Staying on Windows 10 now carries real risk. First — Check Your BIOS Before Anything Else You Might Not Actually Need a Bypass Before running any bypass, open your BIOS and look at two settings. Many computers that fail the Windows 11 compatibility check have TPM 2.0 present in the hard...

Solve common AKS issues with practical troubleshooting techniques for networking, scaling, upgrades, and workloads

Troubleshooting Guide AKS Kubernetes Real Solutions kubectl Azure Kubernetes Service (AKS) Troubleshooting Guide: Real Solutions to Common Problems CrashLoopBackOff at 2am. Pods stuck Pending with no obvious cause. Nodes going NotReady mid-deployment. DNS resolution silently failing in production. Every AKS engineer encounters these — the difference between engineers who panic and engineers who stay calm is knowing the exact sequence of diagnostic commands to run. This guide gives you that sequence, the root cause analysis for each failure mode, and the fix. 3 commands 90% of AKS problems are diagnosed with the same three kubectl commands: describe pod, logs --previous, and get events — in that order, every time Exit 137 The exit code that tells you everything: container killed by SIGKILL — either the Linux OOM killer (memory limit exceeded) or kubelet after grace period expired 5 min The CrashLoopBackOff ceiling: Kubernetes applies exponential backoff (10s → 20s → 40s → 80s → 160s → 3...

Step-by-step guide to deploying scalable AI chatbots on Azure with OpenAI and App Service

Step-by-Step Guide Azure OpenAI App Service Production Python How to Deploy an AI Chatbot on Azure Using Azure OpenAI and App Service From zero to a production-grade AI chatbot: provision Azure OpenAI, write a streaming Flask API backend, deploy it on Azure App Service with Managed Identity, wire in conversation history and content safety, and instrument it with Application Insights — all with complete code and Terraform IaC. No API keys in environment variables. No hardcoded secrets. No half-finished PoC patterns. 7 phases This guide covers the full deployment lifecycle: architecture design → resource provisioning → backend code → App Service deployment → streaming → security → monitoring Zero keys The chatbot authenticates to Azure OpenAI using Managed Identity and DefaultAzureCredential — no API keys stored in environment variables, Key Vault, or code SSE Server-Sent Events stream GPT tokens to the browser as they generate — the same token-by-token typing effect users expect from pr...