Skip to content

Query health endpoint

Arc provides a live health feed at /.cratis/queries/health. The feed is itself a model-bound observable query, publishing the subscription state recorded by the hub health tracker.

The feed exposes cross-user connection IDs, query names, remote IP addresses, user agents, and user identities when available. It allows anonymous access in Development and requires an authenticated caller everywhere else, including Production and Staging. These defaults apply to the direct endpoint and named subscriptions over both WebSocket and SSE hubs. This is diagnostic telemetry, not a public liveness endpoint.

When something goes wrong with real-time data — a component freezes, a query never receives its first result, or a user reports stale data — the question is always: is the issue in the frontend cache, the transport connection, or the backend subscription? The health endpoint gives you a backend-authoritative answer without attaching a debugger or tailing logs.

The query identifiers in the health snapshot use the same fully-qualified format ({TypeFullName}.{MethodName}) that the proxy generator writes into the generated TypeScript proxies. You can match a frontend cache entry to a backend subscriber by name alone.

For a direct stream from a trusted diagnostic client, use the configured host origin and normal credentials:

Terminal window
curl --no-buffer --max-time 30 --header 'Accept: text/event-stream' \
'https://localhost:5001/.cratis/queries/health'

For hub subscriptions, use the fully qualified name Cratis.Arc.Queries.QueryHealth.ObserveHealth and the hub subscription lifecycle. Do not infer a generated TypeScript import path from the HTTP route; use the proxy actually generated into your application’s configured output directory.

Configure ArcOptions.QueryHealth in code or bind its settings from Cratis:Arc:QueryHealth. For example, this appsettings.json excerpt requires the QueryDiagnostics role, including in Development:

{
"Cratis": {
"Arc": {
"QueryHealth": {
"RequireAuthentication": true,
"Roles": "QueryDiagnostics"
}
}
}
}
Option Behavior
RequireAuthentication When unset, anonymous in Development and authenticated elsewhere. Explicit true requires authentication in every environment. Explicit false allows anonymous access when no roles are configured.
Roles Optional comma-separated roles; any one grants access. Entries are trimmed and empty entries ignored. Setting roles requires authentication in every environment, even with RequireAuthentication=false.

Configure real authentication and role mapping on the host. Outside Development, a host without authentication cannot admit callers to the feed. To deliberately restore anonymous access, set Cratis:Arc:QueryHealth:RequireAuthentication=false and leave Roles unset. This exposes cross-user diagnostic data, so use it only on a trusted host or network.

These settings apply only to QueryHealth.ObserveHealth; they do not relax host fallback authorization for other queries. They govern admission, not revocation of an already authorized live stream; use emission guards for ongoing revocation.

A single snapshot has two views of recorded subscription state: connection-centric and query-centric. These fields describe the model inside QueryResult.data, not the outer Arc envelope. Date values are ISO strings on the JSON wire; a generated client may deserialize them to Date.

Field Type Description
connections QueryConnectionHealth[] One entry per tracked hub connection (WebSocket or SSE); empty entries may remain briefly after unsubscribe.
totalConnections number Total count of tracked connections.
totalSubscriptions number Total count of recorded subscriptions across tracked connections.
querySubscriptions QuerySubscriptionAggregate[] Query-centric view — one entry per distinct query name.
Field Type Description
connectionId string Unique connection identifier. WebSocket connections use an incrementing ws-N label; SSE connections use a GUID.
protocol string WebSocket or SSE.
establishedAt Date When the tracker first registered a subscription for the connection.
subscriptions QuerySubscriptionMetadata[] All subscriptions routed through this connection.
Field Type Description
subscriptionId string The client-generated query ID (queryId in the WebSocket protocol).
queryIdentifier string Fully-qualified query name — matches queryName on the generated TypeScript proxy.
readModelType string Fully-qualified name of the read model type only (without the method).
connectedAt Date When this subscription was first established.
clientInfo QuerySubscriptionClientInfo Remote IP, user agent, user identity, and protocol.
lastPingSentAt Date? Last time a keep-alive ping was sent to this subscriber.
lastPongReceivedAt Date? Last time a pong was received back.
lastDataServedAt Date? Last time a data frame was sent to this subscriber.

The query-centric view groups all physical subscribers for a single query name into one entry. The queryName field uses the same identifier format as the frontend proxy’s queryName property, making it straightforward to correlate the two sides.

Field Type Description
queryName string Fully-qualified query name — identical to queryName on the generated TypeScript proxy.
totalSubscriptions number Number of physical subscribers for this query.
subscribers QuerySubscriber[] One entry per physical connection/subscription pair.
Field Type Description
connectionId string The parent connection this subscriber belongs to.
protocol string WebSocket or SSE.
subscriptionId string The client-generated subscription identifier.
connectedAt Date When this subscription was established.
clientInfo QuerySubscriptionClientInfo Remote IP, user agent, user identity, and protocol.
lastPingSentAt Date? Last ping sent.
lastPongReceivedAt Date? Last pong received.
lastDataServedAt Date? Last data frame sent.
Field Type Description
remoteIpAddress string? Client IP address.
userAgent string? Browser or client user-agent string.
userId string? Authenticated user identity, if any.
protocol string WebSocket or SSE.

Illustrative QueryResult.data value, not a captured response or the full envelope:

{
"connections": [
{
"connectionId": "ws-1",
"protocol": "WebSocket",
"establishedAt": "2026-06-10T14:03:00Z",
"subscriptions": [
{
"subscriptionId": "all-authors-main",
"queryIdentifier": "MyApp.Authors.Listing.AllAuthors",
"readModelType": "MyApp.Authors.Listing",
"connectedAt": "2026-06-10T14:03:01Z",
"lastPingSentAt": "2026-06-10T14:04:00Z",
"lastPongReceivedAt": "2026-06-10T14:04:00Z",
"lastDataServedAt": "2026-06-10T14:03:01Z",
"clientInfo": {
"protocol": "WebSocket",
"remoteIpAddress": "127.0.0.1",
"userAgent": "Mozilla/5.0 ...",
"userId": "alice@example.com"
}
}
]
}
],
"totalConnections": 1,
"totalSubscriptions": 1,
"querySubscriptions": [
{
"queryName": "MyApp.Authors.Listing.AllAuthors",
"totalSubscriptions": 1,
"subscribers": [
{
"connectionId": "ws-1",
"protocol": "WebSocket",
"subscriptionId": "all-authors-main",
"connectedAt": "2026-06-10T14:03:01Z",
"lastPingSentAt": "2026-06-10T14:04:00Z",
"lastPongReceivedAt": "2026-06-10T14:04:00Z",
"lastDataServedAt": "2026-06-10T14:03:01Z",
"clientInfo": {
"protocol": "WebSocket",
"remoteIpAddress": "127.0.0.1",
"userAgent": "Mozilla/5.0 ...",
"userId": "alice@example.com"
}
}
]
}
]
}

The hub records both multiplexed WebSocket and multiplexed SSE subscriptions. WebSocket connections have ws-N identifiers; SSE hub connections have GUID identifiers and may carry multiple subscriptions each. Do not confuse this with direct per-query SSE.

The current registration call sites are in the demultiplexer. Do not treat the feed as a complete inventory of direct per-query WebSocket/SSE connections, or of idle hub connections with no registered subscriptions. querySubscriptions groups the subscriptions that were actually recorded; absence from this feed alone does not prove no direct client is watching a query.

The queryName in QuerySubscriptionAggregate and the queryIdentifier in QuerySubscriptionMetadata both use the format {TypeFullName}.{MethodName} — the same string the proxy generator writes as the queryName field on generated TypeScript proxy classes. Because both sides share the same identifier, you can match a frontend cache entry to its backend subscriptions by name.

For example, if the frontend diagnostics report that the cache entry for MyApp.Authors.Listing.AllAuthors is not subscribed, you can verify in the backend health feed whether a subscription for that name exists at all, which connection carries it, and when data was last served.

See Observable Query Diagnostics for how to access the matching frontend diagnostics.