AWS OpenSearch · 2026-10-22 · Advanced arc
AWS OpenSearch Service Advanced Patterns for MCP Monitoring: Vector Search, ISM Lifecycle, Serverless Trade-offs, and Anomaly Detection
A production MCP monitoring system built on Amazon OpenSearch Service eventually outgrows raw indexing and basic querying. At scale, you need four additional capabilities that OpenSearch provides natively but each with configuration traps that bite developers mid-flight: vector search for semantic endpoint discovery, automated index lifecycle management for cost-efficient probe data retention, a serverless option that changes the capacity and API model entirely, and anomaly detection that catches endpoint degradation before binary up/down thresholds fire. The missteps that cost teams days are specific: omitting "index.knn": true on the index stores vector fields as inert blobs with no HNSW graph; writing probe results directly to the backing index name instead of the rollover alias bypasses ISM rollover entirely; using service: 'es' instead of service: 'aoss' in the SigV4 signer returns HTTP 403 with no explanation when connecting to a Serverless collection; and setting the anomaly detector interval to the default 10 minutes on 60-second probe data means the fastest detectable anomaly is 10 minutes old rather than 1 minute old. This guide synthesises four OpenSearch advanced feature areas into three structural patterns for MCP teams. Pattern 1 covers the vector intelligence layer — choosing between the nmslib (deprecated), lucene, and faiss k-NN engines, creating a knn_vector index with correct HNSW parameters, filtering k-NN results without result-count loss using lucene pre-filtering, combining BM25 and vector scores in hybrid search, and monitoring k-NN graph health via the stats API — synthesised from OpenSearch k-NN Vector Search for MCP Servers. Pattern 2 covers the time-series lifecycle model — defining ISM hot-warm-cold state machines with rollover, forced merge, and 90-day delete transitions, the rollover alias pattern that makes ISM trigger correctly, ISM templates for zero-maintenance policy attachment to new rollover indices, forced merge scheduling to avoid p99 query latency spikes, UltraWarm cost model, and the Serverless deployment option with its three collection types, three-layer access policy hierarchy, OCU cost floor, and the long list of managed-domain capabilities it intentionally omits — synthesised from OpenSearch ISM for Probe Time-Series Data and Amazon OpenSearch Serverless for MCP Tools. Pattern 3 covers anomaly detection as an early-warning system — the difference between single-metric, multi-metric, and High Cardinality (HC) detectors, creating an HC detector with per-server-slug RCF sub-models at 1-minute resolution, understanding the warm-up window and its impact on alert suppression, using preview mode to validate feature extraction before going live, wiring detectors to OpenSearch Alerting monitors and webhook destinations, scaling beyond the 1,000-entity HC limit via detector partitioning, and AliveMCP's degraded-status early-warning pipeline that fires before binary down thresholds trip — synthesised from OpenSearch Anomaly Detection for MCP Endpoint Health.
TL;DR
- The vector intelligence layer — k-NN engine selection, index mapping, filtered queries, and hybrid search: OpenSearch supports three k-NN engines: nmslib is deprecated on OpenSearch 2.13+ and must not be used in new deployments; lucene is the correct engine for fewer than 10 million vectors and is the only engine that supports pre-filtering (filter inside the knn query, not post-hoc); faiss handles larger corpora with better throughput at the cost of pre-filtering. Two index-level requirements are mandatory: the index must have
"index.knn": truein its settings, and each vector field must have"type": "knn_vector"with an immutabledimensionmatching your embedding model (1536 for OpenAI ada-002/text-embedding-3-small, 768 for BGE-base, 384 for all-MiniLM). The HNSW parametersm(graph connectivity, default 16) andef_construction(build quality, default 512) are fixed at index creation;ef_search(query recall depth) can be changed per-query. For filtered k-NN with the lucene engine, passfilterinside the knn query — the filter is applied before graph traversal so all k results respect it. For post-filter engines (nmslib, faiss-HNSW), setkto 10× desired results to compensate for filter loss. Hybrid BM25 + k-NN search via thehybridquery type requires a search pipeline with a normalization processor (min-max) and combination processor; this corrects the score scale mismatch between BM25 and cosine similarity. Health check:GET /_plugins/_knn/statsexposesknn_query_errors,cache_capacity_reached, andgraph_index_errors— non-zero values on any of these indicate HNSW graph corruption or JVM heap pressure causing graph eviction and silent fallback to brute-force search. - The time-series lifecycle model — ISM hot-warm-cold, rollover aliases, and the Serverless deployment trade-off: ISM policies are state machines attached to indices — each state defines actions (rollover, force_merge, replica_count change, allocation change, read_only, delete) and transitions (conditions on doc count, size, or index age that move the index to the next state). For probe results, the standard lifecycle is hot (SSD, 7 days or 50 GB per index) → warm (replica reduction + forced merge + UltraWarm allocation) → cold (read-only) → delete at 90 days. Three ISM correctness requirements: rollover only fires when writes go through the write alias (never directly to the index); the warm allocation action (
box_type: warm) requires UltraWarm nodes in the cluster — without them, shards enter a stuck INITIALIZING state that silently blocks the forced merge behind it; ISM templates in the policy definition auto-attach the policy to all new rollover indices, preventing unbounded index accumulation without per-index manual intervention. OpenSearch Serverless offers a different deployment model with no cluster to manage: create a collection of typesearch,timeseries, orvectorsearch, apply encryption → network → data access policies in that order, and write documents. The critical configuration difference: the SigV4 signer must useservice: 'aoss', notservice: 'es'— the latter returns HTTP 403 with no body from the AWS signing layer. Cost: the minimum floor is 2 indexing OCUs + 2 search OCUs at $0.24/OCU-hour, which is approximately $350/month even when idle. This makes Serverless significantly more expensive than at3.medium.searchmanaged domain (~$60/month) for low-to-moderate probe ingest rates. Serverless becomes cost-competitive only above sustained high query burst loads where managed domain auto-scaling costs exceed $350/month. Feature gap: Serverless does not support ISM policies, scroll API, cat APIs, cluster admin operations, anomaly detection, or alerting plugins — all four of the advanced features in this guide require a managed OpenSearch domain. - Anomaly detection as early-warning — HC detectors, RCF warm-up, alerting integration, and entity scaling: OpenSearch's Anomaly Detection plugin runs Random Cut Forest (RCF) models over index data and scores each time window as anomalous or normal based on how much a new data point increases the complexity of the trained forest. Three detector types: single-metric (fleet-wide aggregate anomalies), multi-metric (correlated signals on one index), and High Cardinality (HC) which runs one sub-detector per unique entity value (e.g., one RCF model per server slug). HC is the correct choice for per-endpoint p95 latency detection: each server's baseline is learned independently, so a server that always runs at 800ms does not trigger anomalies while a server that normally runs at 50ms but suddenly hits 400ms does. Three configuration decisions that determine detection quality: set
detector_intervalto1mfor 60-second probe data (the default 10m misses sub-10-minute degradation windows entirely); setwindow_delayto 30 seconds to wait for late-arriving probe data before scoring each interval; setshingle_sizeto 8 (the default) which gives the RCF a context window of 8 consecutive intervals (8 minutes of latency history). Warm-up: forshingle_size=8at 1-minute intervals, the detector producesanomaly_grade: 0for the first 8 minutes after creation and after any gap longer than 8 minutes — do not alert on results with grade 0 and confidence 0. HC entity limit: each HC detector tracks at most 1,000 unique entity values; additional entities above that limit are silently excluded. For AliveMCP monitoring tens of thousands of servers, partition across one HC detector per registry source — MCP.so, Glama, Smithery, PulseMCP — each covering under 1,000 servers, with a global single-metric detector on top for fleet-wide probe pipeline anomalies. Alerting integration: theanomaly_detector_monitortype in OpenSearch Alerting polls the anomaly results index automatically; setmin_anomaly_grade: 0.7andmin_confidence: 0.5as the trigger threshold. AliveMCP promotes a server from "live" to "degraded" status when grade > 0.7 persists for two consecutive intervals, giving Author-tier subscribers a 5–15 minute advance notice before the binary down threshold fires. - End-to-end OpenSearch advanced architecture for MCP monitoring: A complete advanced OpenSearch stack for AliveMCP layers all four capabilities into one coherent data platform. The managed domain (not Serverless — ISM and anomaly detection both require it) hosts two index families:
probe-results-*, a rollover alias family for time-series probe data managed by ISM through hot/warm/cold/delete transitions at a total ~650 GB, andmcp-endpoints-v1, a k-NN index holding 50,000 endpoint embeddings at 768 dimensions for semantic similarity search. The ISM policy rolls over probe-results indices at 50 GB or 7 days, reducing replicas and merging segments on warm transition (90% cost reduction on UltraWarm nodes versus hot SSD), and deletes at 90 days. The k-NN index uses the lucene engine for native pre-filtering, enabling status-filtered similarity queries (find the 5 most similar live servers when a given server goes down) without post-filter result loss. The HC anomaly detector runs on probe-results-* at 1-minute resolution with shingle_size=8, fires early-warning webhooks to Author subscribers when grade > 0.7 for two consecutive minutes, and surfaces the degraded status badge on the public status page before the server trips binary down. An AOSS vectorsearch collection under cost review handles a subset of the k-NN workload as a Serverless experiment — the managed domain remains primary because ISM retention policies and the anomaly detection plugin are unavailable in Serverless and both are load-bearing in the production pipeline.
Pattern 1 — The Vector Intelligence Layer: k-NN Engine Selection, Index Mapping, Filtered Queries, and Hybrid Search
Choosing between nmslib, lucene, and faiss
OpenSearch's k-NN plugin supports three backend engines, each with different trade-offs for the size, filter requirements, and throughput of MCP endpoint embedding workloads.
| Engine | Algorithm | Status | Filter support | Best for |
|---|---|---|---|---|
| nmslib | HNSW | Deprecated (OpenSearch 2.13+) | Post-filter only | Legacy deployments — do not use in new indexes |
| lucene | HNSW | Stable | Native pre-filter via filter inside knn query | <10M vectors, filtered queries, small-to-medium clusters |
| faiss | HNSW or IVF | Stable | Post-filter (HNSW) or efficient inner filter (IVF+PQ) | >10M vectors, high-throughput, optional GPU acceleration |
For AliveMCP's endpoint similarity search — approximately 50,000 MCP server description embeddings at 768 dimensions — the lucene engine is the correct choice. It supports native pre-filtering (filter applied before HNSW graph traversal, not after), 50,000 vectors at 768 dimensions occupies roughly 150 MB on-heap, and the workload is read-heavy with low concurrent query volume. nmslib is deprecated on OpenSearch 2.13 and later; any index that still uses it silently falls back to lucene behaviour in queries but the deprecation warning fills CloudWatch logs with noise and the engine receives no further optimisations.
Index mapping: knn_vector field type and the index.knn requirement
k-NN indexing is opt-in at the index level. Both a settings flag and a field mapping type are required — omitting either one produces a vector field that stores data correctly but builds no HNSW graph, returning zero results silently on k-NN queries.
import { Client } from '@opensearch-project/opensearch';
import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
const os = new Client({
...AwsSigv4Signer({
region: process.env.AWS_REGION ?? 'us-east-1',
service: 'es', // 'es' for managed domain; 'aoss' for Serverless — see Pattern 2
getCredentials: () => defaultProvider()(),
}),
node: `https://${process.env.OPENSEARCH_DOMAIN_ENDPOINT}`,
});
await os.indices.create({
index: 'mcp-endpoints-v1',
body: {
settings: {
'index.knn': true, // Required — without this, knn_vector fields are inert blobs
'index.knn.algo_param.ef_search': 512, // Query-time graph traversal depth (default 512)
number_of_shards: 2,
number_of_replicas: 1,
},
mappings: {
properties: {
server_slug: { type: 'keyword' },
display_name: { type: 'text', analyzer: 'standard' },
registry: { type: 'keyword' }, // mcp.so, glama, smithery, pulsemcp, etc.
status: { type: 'keyword' }, // live, dead, degraded
checked_at: { type: 'date' },
description_embedding: {
type: 'knn_vector',
dimension: 768, // Must match embedding model output exactly — immutable after creation
method: {
name: 'hnsw',
engine: 'lucene', // lucene for filtered queries and <10M vectors
space_type: 'cosinesimil', // cosine similarity for text embeddings
parameters: {
m: 16, // Graph connectivity: higher = better recall, more memory (8–64)
ef_construction: 256, // Build quality: higher = better graph, slower indexing (128–512)
},
},
},
},
},
},
});
The dimension parameter is immutable after index creation. Common values: 1536 for OpenAI text-embedding-ada-002 and text-embedding-3-small, 3072 for text-embedding-3-large, 768 for BGE-base-en-v1.5 and E5-base, 384 for all-MiniLM-L6-v2. AliveMCP uses 768-dimensional BGE-base embeddings run locally, which avoids the per-embedding API cost for a corpus that refreshes infrequently.
HNSW parameters: m, ef_construction, and ef_search
Three parameters govern HNSW recall quality and resource usage. Two are fixed at index creation; one can be tuned per-query without re-indexing.
| Parameter | Default | Effect | Trade-off |
|---|---|---|---|
m | 16 | Edges per node in the HNSW graph (graph connectivity) | Higher = better recall, more memory (~8 bytes × m × vector count) |
ef_construction | 512 | Candidate list size during graph construction | Higher = better graph quality, slower bulk indexing; has no effect after index is built |
ef_search | 512 | Candidate list size during query traversal | Higher = better recall, slower queries; set per-query via index setting or URL param |
For 50,000 endpoint embeddings at 768 dimensions with m=16: HNSW graph overhead is approximately 50,000 × 16 × 8 bytes × 2 layers ≈ 12.8 MB. Raw vector storage is 50,000 × 768 × 4 bytes ≈ 154 MB. Total on-heap footprint ~167 MB — well within a single shard on even the smallest search instance type.
Filtered k-NN queries: pre-filter vs post-filter
The most common k-NN mistake in MCP monitoring is using a post-filter engine (nmslib, faiss-HNSW) with a selective filter — for example, filtering for only "live" servers when 70% of monitored endpoints are dead or degraded. Post-filter retrieves k approximate nearest neighbours from the full corpus, then applies the filter, which discards most results and returns far fewer than k matches. With the lucene engine, the filter is applied before graph traversal — the HNSW search operates only within the matching subset, so all k results respect the filter regardless of selectivity.
// lucene engine: pre-filter — all k results are guaranteed to match the filter
async function findSimilarLiveEndpoints(
queryEmbedding: number[],
excludeSlug: string,
k = 5,
): Promise {
const response = await os.search({
index: 'mcp-endpoints-v1',
body: {
size: k,
query: {
knn: {
description_embedding: {
vector: queryEmbedding,
k,
// lucene pre-filter: applied BEFORE graph traversal
// With faiss/nmslib post-filter, you'd need k = desired_results / filter_selectivity
filter: {
bool: {
must: [
{ term: { status: 'live' } },
],
must_not: [
{ term: { server_slug: excludeSlug } }, // Exclude the server that went down
],
},
},
},
},
},
_source: ['server_slug', 'display_name', 'registry', 'status'],
},
});
return response.body.hits.hits.map((hit: any) => ({
...hit._source,
similarity: hit._score,
}));
}
Hybrid BM25 + k-NN search
Pure k-NN queries miss exact keyword matches: searching for "PostgreSQL MCP server" by embedding may rank Redis or MySQL servers highly if their descriptions share semantic overlap around "database" and "query". Hybrid search combines a BM25 keyword score with a k-NN vector score using a hybrid query type (available in OpenSearch 2.10+), but the two score scales are incompatible — BM25 scores are unbounded floats while cosine similarity scores are in [0, 1]. A normalization processor in a search pipeline corrects this.
// One-time pipeline creation — run at deploy time
await os.transport.request({
method: 'PUT',
path: '/_search/pipeline/hybrid-search-pipeline',
body: {
description: 'Normalize and combine BM25 and k-NN scores for MCP endpoint search',
phase_results_processors: [
{
'normalization-processor': {
normalization: { technique: 'min_max' },
combination: { technique: 'arithmetic_mean', parameters: { weights: [0.3, 0.7] } },
// 0.3 weight for BM25, 0.7 for k-NN — tune based on search quality
},
},
],
},
});
// Hybrid search query
const hybridResults = await os.search({
index: 'mcp-endpoints-v1',
body: {
search_pipeline: 'hybrid-search-pipeline',
query: {
hybrid: {
queries: [
{
match: {
display_name: { query: searchText, boost: 1.0 },
},
},
{
knn: {
description_embedding: {
vector: queryEmbedding,
k: 20, // Retrieve more k-NN candidates; pipeline re-ranks after normalization
},
},
},
],
},
},
size: 10,
_source: ['server_slug', 'display_name', 'registry', 'status'],
},
});
k-NN plugin health monitoring
The k-NN stats API exposes graph build errors, query errors, and cache pressure — the three signals that indicate vector search is degraded without any query latency spike yet visible to users.
const stats = await os.transport.request({
method: 'GET',
path: '/_plugins/_knn/stats',
});
const nodeStats = stats.body.nodes[Object.keys(stats.body.nodes)[0]];
const {
knn_query_errors, // Non-zero: index corruption or dimension mismatch
cache_capacity_reached, // True: HNSW graphs evicted from JVM heap — brute-force fallback active
graph_index_errors, // Non-zero: bulk indexing failures dropped vectors
} = nodeStats;
if (knn_query_errors > 0 || cache_capacity_reached || graph_index_errors > 0) {
// Alert: vector search degraded; similarity queries may return incorrect results
// cache_capacity_reached = true means k-NN is silently running brute-force (O(n) vs O(log n))
// Resolve by increasing knn.memory.circuit_breaker.limit (default 50% JVM heap) or upsizing instance
}
cache_capacity_reached: true is the most dangerous silent failure mode in k-NN workloads. When the HNSW graph is evicted from the k-NN circuit-breaker-controlled cache, subsequent queries fall back to brute-force exhaustive search — orders of magnitude slower, but still returning results. From a caller's perspective, the k-NN feature appears to be working. From a latency perspective, p99 query time for 50,000 vectors goes from ~5ms to ~500ms. The only way to detect this without monitoring is noticing the latency spike. The stats API makes it explicit.
Pattern 2 — The Time-Series Lifecycle Model: ISM Hot-Warm-Cold, Rollover Aliases, and the Serverless Deployment Trade-off
ISM policy structure: states, actions, and transitions
An ISM policy defines a state machine applied to one or more indices. Each state has actions (operations to perform once while in that state) and transitions (conditions that move the index to the next state). The ISM worker checks each managed index every 5 minutes by default.
For AliveMCP's probe results — approximately 83 documents per second at 300,000 monitored endpoints — a single index reaches 50 GB in about 7 days. The lifecycle policy manages 13 rolling indices concurrently, totalling ~650 GB of data, with the oldest data on UltraWarm nodes at roughly 90% lower cost per GB than hot SSD.
const lifecyclePolicy = {
policy: {
description: 'AliveMCP probe results: 7d hot, 30d warm (UltraWarm), 60d cold, delete at 90d',
default_state: 'hot',
states: [
{
name: 'hot',
actions: [
{
rollover: {
min_doc_count: 50_000_000, // 50M probe results (~7 days)
min_size: '50gb',
min_index_age: '7d', // Whichever condition is met first triggers rollover
},
},
],
transitions: [
{ state_name: 'warm', conditions: { min_rollover_age: '1d' } },
],
},
{
name: 'warm',
actions: [
{ replica_count: { number_of_replicas: 0 } }, // Cut storage cost before merge
{ force_merge: { max_num_segments: 1 } }, // 20-40% storage reduction; takes 10-40min
{ allocation: { require: { box_type: 'warm' } } }, // Move to UltraWarm nodes (S3-backed)
// WARNING: allocation to warm requires UltraWarm nodes in the cluster.
// Without them, shards enter INITIALIZING and force_merge never runs.
],
transitions: [
{ state_name: 'cold', conditions: { min_index_age: '30d' } },
],
},
{
name: 'cold',
actions: [
{ read_only: {} },
{ allocation: { require: { box_type: 'cold' } } },
],
transitions: [
{ state_name: 'delete', conditions: { min_index_age: '90d' } },
],
},
{
name: 'delete',
actions: [{ delete: {} }],
transitions: [],
},
],
ism_template: [
{
index_patterns: ['probe-results-*'],
priority: 100, // Higher priority wins when multiple templates match an index
},
],
},
};
The rollover alias pattern: why writing directly to the index breaks ISM
ISM's rollover action only fires when writes go through an alias with "is_write_index": true. Writing directly to the backing index name — even the current one — bypasses ISM's rollover trigger entirely. The index accumulates without limit while ISM's other state transitions (warm, cold, delete) still run on the index at its existing age, creating the paradox of data being deleted at 90 days while the index itself never rolled over and therefore contains mixed-age data spanning multiple rollover periods.
// Create the initial backing index with write and read aliases
await os.indices.create({
index: 'probe-results-000001',
body: {
aliases: {
'probe-results-write': { is_write_index: true }, // ISM rollover fires on this alias
'probe-results-read': {}, // Read queries span all backing indices
},
settings: {
'index.plugins.index_state_management.policy_id': 'probe-results-lifecycle',
},
},
});
// Correct: write via the alias — ISM rollover triggers when min_doc_count/min_size/min_age met
await os.index({
index: 'probe-results-write',
body: { server_slug, status, latency_ms, checked_at: new Date() },
});
// Wrong: direct index write — rollover never fires, index grows unbounded
// await os.index({ index: 'probe-results-000001', body: probeResult });
// After rollover: backing sequence becomes probe-results-000001, -000002, -000003...
// probe-results-read alias automatically spans all backing indices for historical queries
ISM templates: zero-maintenance policy attachment
Without an ISM template, each new rollover index (probe-results-000002 through probe-results-000099) is created without a policy attached. The ISM worker ignores it; data accumulates without lifecycle management. The ism_template array in the policy definition auto-attaches the policy to any index whose name matches the pattern at creation time — set it once, and every rollover index inherits the lifecycle policy automatically.
To verify a new index received the template-attached policy, check its settings after creation:
const settings = await os.indices.getSettings({ index: 'probe-results-000002' });
const policyId = settings.body['probe-results-000002']?.settings?.index
?.plugins?.index_state_management?.policy_id;
if (!policyId) {
// Template did not attach — check that ism_template index_patterns match the index name
// and that the ISM template priority is high enough to win over other matching templates
console.error('probe-results-000002 has no ISM policy attached');
}
Forced merge: I/O impact and scheduling strategy
Forced merge rewrites all Lucene segments in a shard into a single segment, reducing storage 20–40% and cutting query time for aggregation-heavy analytics (90-day histograms drop from ~800ms with 200 segments to ~120ms with 1 merged segment). The operation is disk I/O saturating and takes 10–40 minutes for a 50 GB index.
ISM executes actions within a state sequentially in definition order. Placing replica_count and allocation before force_merge in the warm state ensures shard count is reduced and the UltraWarm migration has started before the merge begins. For production environments with strict p99 SLAs, split the warm state into two: warm-setup for replica reduction and allocation, and warm-merge triggered min_index_age: 2h after warm-setup — this constrains forced merge to run at least 2 hours after the last write, outside the probe peak window.
OpenSearch Serverless: when to use it and when not to
Amazon OpenSearch Serverless removes cluster sizing and scaling decisions. You create a collection, apply three policy layers, and write documents. The SigV4 service name is 'aoss', not 'es' — this single character difference is the most common Serverless integration failure.
| Collection type | Storage | Supported APIs | Best for |
|---|---|---|---|
| search | Hot SSD | Full query DSL, aggregations, PPL, SQL | User-facing search, analytics, log exploration |
| timeseries | Cost-optimised tiered (S3-backed) | Ingest + time-range queries only; no full-text | High-volume metrics, traces, logs (write-heavy) |
| vectorsearch | Hot SSD with k-NN graph | k-NN, BM25, hybrid search | Semantic search, RAG pipelines, MCP endpoint discovery |
// OpenSearch Serverless client — 'aoss' not 'es'
import { Client } from '@opensearch-project/opensearch';
import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
const serverlessClient = new Client({
...AwsSigv4Signer({
region: process.env.AWS_REGION ?? 'us-east-1',
service: 'aoss', // 'aoss' for Serverless — using 'es' returns HTTP 403 with no error body
getCredentials: () => defaultProvider()(),
}),
node: `https://${process.env.AOSS_COLLECTION_ENDPOINT}`,
// Endpoint format: {collection-id}.{region}.aoss.amazonaws.com
});
The cost floor for Serverless is 2 indexing OCUs + 2 search OCUs at $0.24/OCU-hour = approximately $0.48/hour = $350/month even when idle. A managed t3.medium.search domain costs ~$60/month. Serverless is only cheaper when managed domain auto-scaling to handle burst queries would push the cost above $350/month — roughly at 50,000+ concurrent queries per hour with unpredictable bursts. At AliveMCP's scale — 83 writes/second, moderate query volume — the managed domain is definitively cheaper.
The feature gap is the more important constraint for most MCP monitoring teams. Serverless does not support ISM policies, the scroll API, cat APIs, cluster admin APIs, the anomaly detection plugin, or the alerting plugin. All four advanced features in this guide require a managed domain.
| Capability | Managed domain | Serverless |
|---|---|---|
| Index State Management (ISM) | Full support | Not available |
| Scroll API | Full support | Not available — use search_after pagination |
| Anomaly detection plugin | Full support | Not available |
| Alerting plugin | Full support | Not available |
| k-NN search | All engines | vectorsearch collection only |
| UltraWarm / Cold storage | Full support | Storage managed automatically — no control |
Cat APIs (/_cat/*) | Full support | Not available |
Pattern 3 — Anomaly Detection as Early-Warning: HC Detectors, RCF Warm-up, Alerting Integration, and Entity Scaling
Why binary thresholds miss the earliest signal
Binary up/down monitoring answers "did the probe succeed?" — it fires when a server is already failing. In practice, MCP server failures are rarely instantaneous. They are preceded by latency creep: p95 response time climbing from 50ms to 200ms to 800ms over 3–15 minutes before the first timeout. Binary monitoring misses this entire window. OpenSearch Anomaly Detection scores each time window against an individually trained baseline per server, catching the "this server is behaviorally unusual" signal before the "this server is timing out" threshold fires.
HC detector creation: one RCF model per server slug
A High Cardinality (HC) detector uses a category_field (a keyword field) to partition the training data — one RCF model sub-detector runs per unique value of that field. For MCP endpoint monitoring, category_field: "server_slug" gives each server its own baseline. A server that always takes 800ms does not alert; a server that normally takes 50ms but suddenly hits 400ms does.
// Create HC anomaly detector for per-server latency anomalies
const detectorResponse = await os.transport.request({
method: 'POST',
path: '/_plugins/_anomaly_detection/detectors',
body: {
name: 'mcp-endpoint-latency-hc',
description: 'Per-server-slug p95 latency anomaly detection for MCP endpoints',
time_field: 'checked_at',
indices: ['probe-results-*'], // All probe-results rollover indices
feature_attributes: [
{
feature_name: 'p95_latency_ms',
feature_enabled: true,
aggregation_query: {
p95_latency_ms: {
percentiles: { field: 'latency_ms', percents: [95] },
},
},
},
],
filter_query: {
bool: {
must: [
{ term: { probe_type: 'http_check' } },
{ term: { maintenance_mode: false } }, // Exclude servers in maintenance
],
},
},
detector_interval: {
period: { interval: 1, unit: 'Minutes' }, // 1m — critical: default 10m is too coarse for 60s probes
},
window_delay: {
period: { interval: 30, unit: 'Seconds' }, // Wait 30s for late-arriving probe data
},
shingle_size: 8, // RCF context window: 8 × 1m = 8 minutes of history
category_field: ['server_slug'], // HC: one sub-detector per unique server_slug
},
});
const detectorId = detectorResponse.body._id;
RCF algorithm: shingle size and the warm-up window
The Random Cut Forest algorithm builds a forest of random binary trees over a sliding window of shingle_size consecutive data points. Each point in the shingle includes all feature values at that interval, giving the model a multi-dimensional view of recent history. A new data point is anomalous if adding it to the forest increases its complexity more than expected given the trained baseline.
| Parameter | Default | Recommendation for 60s probe data |
|---|---|---|
detector_interval | 10m | 1m — detect anomalies within one probe cycle |
shingle_size | 8 | 8 — gives 8-minute context window at 1m interval |
window_delay | 0 | 30s–60s — wait for late-arriving ingest |
Warm-up: the RCF forest requires at least shingle_size × interval data points (8 minutes at shingle_size=8, interval=1m) before producing reliable scores. During warm-up, the detector sets anomaly_grade: 0 for all intervals regardless of actual latency. The same warm-up restarts after any data gap exceeding 8 minutes — a server that stops receiving probes for 10 minutes and then resumes will have 8 minutes of suppressed anomaly detection. Never alert on results where both anomaly_grade == 0 and confidence == 0.
Preview mode: validate feature extraction before starting real-time detection
Preview mode runs the detector against historical data without writing to the anomaly results index. Use it before starting the detector to confirm that the feature aggregation returns non-null values and that the anomaly rate on normal data is in the 0.5–5% range.
const preview = await os.transport.request({
method: 'POST',
path: `/_plugins/_anomaly_detection/detectors/${detectorId}/_preview`,
body: {
period_start: Date.now() - 7 * 24 * 60 * 60 * 1000,
period_end: Date.now(),
},
});
const sampleResults = preview.body.anomaly_result.slice(0, 5);
for (const result of sampleResults) {
if (result.feature_data[0].data === null) {
// Feature returned null: check field name ('latency_ms' exists in probe-results-* ?)
// and that the index_patterns in the detector match at least one existing index
console.warn('Feature null — verify field name and index pattern');
}
}
const anomalyRate = preview.body.anomaly_result.filter(r => r.anomaly_grade > 0).length
/ preview.body.anomaly_result.length;
// 0.5–5% is healthy; >10% means the feature is too noisy (use a different aggregation or longer interval)
// 0% means the feature has no variance (check data quality — are all latency_ms values identical?)
console.log(`Preview anomaly rate: ${(anomalyRate * 100).toFixed(1)}%`);
Reading anomaly results and starting real-time detection
// Start real-time detection
await os.transport.request({
method: 'POST',
path: `/_plugins/_anomaly_detection/detectors/${detectorId}/_start`,
});
// Query anomaly results for a specific server in the last hour
const anomalyResults = await os.search({
index: '.opendistro-anomaly-results-history-*', // System index for anomaly results
body: {
query: {
bool: {
must: [
{ term: { detector_id: detectorId } },
{ term: { 'entity.0.value': 'my-mcp-server-slug' } }, // HC entity filter
{ range: { data_start_time: { gte: 'now-1h' } } },
{ range: { anomaly_grade: { gt: 0.5 } } }, // Only significant anomalies
],
},
},
sort: [{ data_start_time: 'desc' }],
_source: ['data_start_time', 'anomaly_grade', 'confidence', 'feature_data', 'entity'],
},
});
Alerting integration: wiring the detector to a webhook destination
// Create alerting monitor that fires when anomaly_grade > 0.7 and confidence > 0.5
await os.transport.request({
method: 'POST',
path: '/_plugins/_alerting/monitors',
body: {
type: 'monitor',
name: 'mcp-endpoint-latency-anomaly-alert',
monitor_type: 'anomaly_detector_monitor',
enabled: true,
schedule: { period: { interval: 1, unit: 'MINUTES' } },
inputs: [{ anomaly_detector: { detector_id: detectorId } }],
triggers: [
{
anomaly_detector_trigger: {
name: 'high-anomaly-grade',
severity: '2',
min_anomaly_grade: 0.7, // Only fire for high-confidence anomalies
min_confidence: 0.5, // Suppresses warm-up period (confidence is 0 during warm-up)
},
},
],
actions: [
{
name: 'notify-alivemcp-webhook',
destination_id: 'WEBHOOK_DESTINATION_ID',
message_template: {
source: `{
"text": "MCP Endpoint Degraded: {{ctx.results[0].hits.hits[0]._source.entity[0].value}} — grade={{ctx.results[0].hits.hits[0]._source.anomaly_grade}} confidence={{ctx.results[0].hits.hits[0]._source.confidence}}"
}`,
},
action_execution_policy: {
action_execution_scope: 'PER_ALERT', // One notification per unique anomalous server
},
},
],
},
});
HC entity limit: scaling beyond 1,000 server slugs
Each HC detector tracks at most 1,000 unique entity values. When more than 1,000 unique server slugs appear in the training data, the detector's priority queue retains the 1,000 most frequently observed entities — new or infrequently probed servers are silently excluded from anomaly detection. No error is raised; the excluded servers simply never have anomaly scores computed.
To monitor more than 1,000 MCP servers with per-entity detection, partition across multiple HC detectors. AliveMCP's strategy: one HC detector per major registry source (MCP.so, Glama, Smithery, PulseMCP), each covering approximately 500 servers, with a global single-metric detector monitoring fleet-wide probe pipeline anomalies (total probe throughput, aggregate p95 latency across all servers) as a backstop.
// Check how many entity models are active in an HC detector
const entityProfile = await os.transport.request({
method: 'GET',
path: `/_plugins/_anomaly_detection/detectors/${detectorId}/profile/models`,
});
const activeEntities = entityProfile.body.entities?.length ?? 0;
console.log(`Active entity models: ${activeEntities} / 1000 max`);
// If approaching 1000, provision a second HC detector for the overflow registry
Failure modes reference
| Symptom | Root cause | Resolution |
|---|---|---|
| k-NN queries return zero results | "index.knn": true missing from index settings; vectors stored but no HNSW graph built | Re-create index with "index.knn": true and re-index all documents |
| Filtered k-NN returns fewer than k results | Using nmslib or faiss-HNSW post-filter with a selective filter (e.g., status=live when 90% are dead) | Switch to lucene engine for pre-filter support, or set k to 10× desired results |
| AOSS returns HTTP 403 with no body | SigV4 signer using service: 'es' instead of service: 'aoss' | Change SigV4 service to 'aoss' |
| ISM rollover never triggers | Probe data written directly to the backing index instead of through the write alias | Change client to write to probe-results-write alias, not probe-results-000001 |
| Warm-tier shards stuck in INITIALIZING | ISM warm allocation action set box_type: warm but no UltraWarm nodes in cluster | Add UltraWarm nodes, or remove warm allocation action if single-tier cluster |
| All anomaly grades are 0 | Detector in warm-up period (first shingle_size × interval = 8 minutes), or data gap > 8 minutes reset warm-up | Wait for warm-up to complete; check for indexing gaps in probe data |
| HC detector missing some servers | More than 1,000 unique server slugs — priority queue excluded low-frequency entities | Partition across multiple HC detectors, one per registry or server tier |
| k-NN queries suddenly slow (10–100×) | cache_capacity_reached: true — HNSW graphs evicted from JVM heap, brute-force search active | Increase knn.memory.circuit_breaker.limit cluster setting or upsize instance type |
| Forced merge blocking p99 latency | ISM force_merge action running during query peak hours on warm index | Split warm state into two: warm-setup (immediate) and warm-merge (2h delay) to move merge outside peak window |
| ISM template not attaching to new rollover indices | Template index_patterns don't match new index name, or another template has higher priority | Verify ism_template.index_patterns matches the rollover naming pattern; increase template priority |
Related guides
- OpenSearch k-NN Vector Search for Semantic MCP Endpoint Discovery
- OpenSearch Index State Management for Probe Time-Series Data
- Amazon OpenSearch Serverless — collection types, access policies, OCU model
- OpenSearch Anomaly Detection for MCP Endpoint Health
- OpenSearch Overview for MCP Servers — bulk indexing, scroll, SigV4 auth
- How to Monitor an MCP Server
- MCP Server Uptime Monitoring
- AWS MSK for MCP Server Probe Pipelines: Event Architecture, IAM Auth, and Operational Reliability
- AWS MemoryDB for MCP Servers: Durable State, ACL Security, and Cost-Efficient Probe Archives