Observability
The project **Overview** tab shows what is happening in an environment right now: who is connected over WebSocket, what they are subscribed to, and which database calls are slow. Everything is scoped to the environment selected in the dashboard.
Live connections
The Active connections tile counts the WebSocket connections currently open to the environment, and the Live connections panel lists each one with:
- Type:
client(browser or app) orserver(an SSR backend or server SDK connected with?type=server, which also receives cron triggers) - User: the authenticated user id (the token's
subclaim) when the connection was opened with an end-user token, otherwise Anonymous - Connected since: when the socket opened
- Subscriptions: the query or function names it is subscribed to, plus any log streams
Only metadata is shown. Tokens, claims, email addresses and subscription arguments are never returned.
Connections opened without an explicit environment (for example /ws/{projectId}) count towards the project's default environment.
The dashboard home shows an account-wide Active connections total across all of your projects and environments.
Slow queries
Every database call is timed, both calls made from functions through ctx.db and direct calls to the generic CRUD endpoints (/query and /mutation). Any call that takes at least the environment's threshold is recorded with:
| Field | Description |
|---|---|
| Called from | The function that made the call. Direct CRUD calls show Direct CRUD |
| Table | The table the call touched |
| Operation | The CRUD operation, for example findMany or update |
| Duration | How long the database call took, in milliseconds |
| When | When the call finished |
Select a function name to open it in the Functions tab.
The threshold defaults to 100 ms per environment. Select the threshold next to the panel title to change it (1 ms to 60,000 ms). The setting is stored in the environment's database, so it survives restarts and applies to that environment only.
The latest 200 slow calls per environment are kept; older entries are dropped first. History is held in memory, so it resets when the Tether server restarts or is redeployed. Use Clear to empty it yourself.
Execution timing
Every function execution records where its time went, not just a total duration. The breakdown has two layers.
The additive phases run one after another and together make up almost all of the total duration:
queueMs: waiting for a runtime share and a free isolate before the run started.compileMs: per-run setup (building the environment's context and the function's script).runMs: the handler itself, including microtasks and timers.
Inside runMs, the runtime also tallies the time the handler spent waiting on host work. These figures are an "of which" breakdown, not extra phases, so they are never added on top of the three above:
dbMs/dbCalls: database CRUD calls.bridgeMs/bridgeCalls: bridge calls into other projects.storageMs/storageCalls: storage (asset) operations.
The dashboard shows this as a stacked bar in the function Logs tab (per recent execution) and in the playground. Each execution in the GET /executions response carries a timings object with these fields, and the response's stats block adds a project-wide avgTimings average. Executions that failed before the handler ran carry a zeroed breakdown. Operators also get one structured tracing::info! line per execution with the full breakdown.
Example GET /executions response:
{
"executions": [
{
"id": "9c2f…",
"functionName": "orders.process",
"type": "mutation",
"status": "success",
"durationMs": 48,
"timestamp": "2026-10-20T09:13:02Z",
"startedAt": "2026-10-20T09:13:02Z",
"timings": {
"queueMs": 2,
"compileMs": 6,
"runMs": 40,
"dbMs": 31,
"dbCalls": 3,
"bridgeMs": 0,
"bridgeCalls": 0,
"storageMs": 0,
"storageCalls": 0
}
}
],
"totalCount": 1,
"stats": {
"total": 1,
"successful": 1,
"failed": 0,
"successRate": 100.0,
"avgDurationMs": 48.0,
"avgTimings": {
"queueMs": 2,
"compileMs": 6,
"runMs": 40,
"dbMs": 31,
"dbCalls": 3,
"bridgeMs": 0,
"bridgeCalls": 0,
"storageMs": 0,
"storageCalls": 0
},
"byFunction": []
}
}
Data retention
Function logs, execution records and cron run history are kept for 30 days, then removed by a daily job. The organisation audit log is kept for 400 days, so a full year is always available.
API
All per-project routes are environment-scoped and accept a dashboard session or an API key for the same environment. OAuth2 tokens need the logs permission.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/projects/{id}/env/{env}/connections |
Live connections for the environment |
GET |
/api/v1/projects/{id}/env/{env}/slow-queries?limit=50 |
Threshold and recent slow calls, newest first |
PATCH |
/api/v1/projects/{id}/env/{env}/slow-queries/config |
Set the threshold: { "thresholdMs": 250 } |
DELETE |
/api/v1/projects/{id}/env/{env}/slow-queries |
Clear the history |
GET |
/api/v1/connections/summary |
Connection counts for every project you own (dashboard session only) |
Example connections response:
{
"total": 1,
"subscriptionCount": 2,
"authenticatedCount": 1,
"connections": [
{
"id": "6f1c…",
"connectionType": "client",
"connectedAt": "2026-10-18T09:12:44Z",
"authenticated": true,
"userId": "user_123",
"subscriptions": ["posts.list", "comments.forPost"],
"logSubscriptions": []
}
]
}
Example slow queries response:
{
"thresholdMs": 100,
"defaultThresholdMs": 100,
"capacity": 200,
"entries": [
{
"id": "b2a4…",
"functionName": "posts.listRecent",
"table": "posts",
"operation": "findMany",
"source": "function",
"durationMs": 182.4,
"thresholdMs": 100,
"timestamp": "2026-10-18T09:13:02Z"
}
]
}