Skip to main content

Query API

LynxDB exposes three query-facing paths:

  • POST /api/v1/query for JSON-envelope query execution
  • POST /api/v1/query/stream for NDJSON export
  • GET /api/v1/query/explain for parse and plan inspection

Async work created by POST /query is tracked under /api/v1/query/jobs/....

POST /query​

Execute a LynxFlow query and return either final results or a job handle.

Request Body​

FieldTypeRequiredDescription
qstringYes*Query text
querystringYes*Alias for q
fromstringNoStart time bound
earlieststringNoAlias for from
tostringNoEnd time bound
lateststringNoAlias for to
limitintegerNoResult row limit. Defaults to query.default_result_limit, capped by query.max_result_limit
offsetintegerNoOffset for paginated event or aggregate results
waitnumberNoExecution mode selector
profilestringNoProfiling level: basic, full, or trace
variablesobjectNoReplaces $name tokens in the query with quoted, escaped string values
formatstringNoResponse format. Only empty or json are accepted on this endpoint. Use POST /api/v1/query/stream for NDJSON output
languagestringNoQuery language. Only lynxflow (or omitted) is accepted

* One of q or query is required.

Query Language​

All queries execute as LynxFlow. The language field is optional; when present, only "lynxflow" is accepted. Requesting "spl2" returns 400 VALIDATION_ERROR with migration guidance:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "language \"spl2\" is no longer supported; migrate queries to LynxFlow — see CHANGELOG for migration guide"
}
}

Successful responses include meta.language: "lynxflow".

Execution Modes​

wait controls when the server returns:

wait valueModeBehavior
omitted or nullsync windowWait until completion. If the query does not finish within query.sync_timeout (default 30s), LynxDB detaches the job and returns 202 Accepted with a job handle
0asyncReturn 202 Accepted immediately with a job handle
positive numberhybridWait up to N seconds. If the query is still running, return the same 202 Accepted job handle

Event Result Example​

curl -s localhost:3100/api/v1/query \
-H 'Content-Type: application/json' \
-d '{
"q": "from main | where level == \"error\" | head 2",
"from": "-1h"
}' | jq .
{
"data": {
"type": "events",
"events": [
{
"_time": "2026-02-14T14:52:01.234Z",
"_raw": "level=ERROR status=502 uri=/api/users",
"source": "nginx",
"host": "web-01",
"level": "ERROR",
"status": 502,
"uri": "/api/users"
}
],
"total": 1247,
"has_more": true
},
"meta": {
"took_ms": 89,
"scanned": 12400000,
"query_id": "qry_7f3a...",
"language": "lynxflow"
}
}

Aggregate Result Example​

curl -s localhost:3100/api/v1/query \
-H 'Content-Type: application/json' \
-d '{
"q": "from main | where source == \"nginx\" and status >= 500 | stats count() by uri | sort -count | head 10",
"from": "-1h"
}' | jq .
{
"data": {
"type": "aggregate",
"columns": ["uri", "count"],
"rows": [
["/api/v1/users", 1247],
["/api/v1/orders", 893]
],
"total_rows": 42,
"has_more": true
},
"meta": {
"took_ms": 34,
"scanned": 12400000,
"query_id": "qry_8b2c..."
}
}

Async or Promoted Job Handle Example​

When a query is explicitly async, or when it misses the sync window, POST /query returns 202 Accepted:

{
"data": {
"type": "job",
"job_id": "qry_9c1d4e",
"status": "running"
},
"meta": {
"query_id": "qry_9c1d4e"
}
}

Current handlers return a minimal job handle only. They do not include query, from, to, or partial result rows in the 202 response body.

Variable Substitution​

variables replaces $name tokens with quoted string literals before planning:

curl -s localhost:3100/api/v1/query \
-H 'Content-Type: application/json' \
-d '{
"q": "from main | where source == $source and level == $level | head 5",
"variables": {
"source": "nginx",
"level": "error"
}
}' | jq .

Response Behavior​

  • POST /query always returns the standard JSON envelope.
  • For NDJSON export, use POST /query/stream.
  • format values other than json are rejected with 400 VALIDATION_ERROR.
  • Removed SPL2 stages (eval, rex, table, timechart, ...) are rejected at parse time with 400 INVALID_QUERY and a suggestion for the LynxFlow equivalent.

Error Responses​

StatusCodeWhen
400INVALID_JSONRequest body is not valid JSON
400INVALID_QUERYParse or planning failed
400QUERY_TOO_LARGEQuery exceeds query.max_query_length
400QUERY_MEMORY_EXCEEDEDQuery exceeded its per-query memory budget
400VALIDATION_ERRORQuery is missing, format is not json, or language is not lynxflow
401AUTH_REQUIRED / INVALID_TOKENAuthentication failure
403FORBIDDENToken lacks query scope
429TOO_MANY_REQUESTSQuery concurrency limit reached
503QUERY_POOL_EXHAUSTEDGlobal query memory pool is exhausted

GET /query​

GET convenience variant for simple queries.

Query Parameters​

ParameterRequiredDescription
qYesQuery text
fromNoStart time bound
toNoEnd time bound
limitNoResult row limit
formatNoOptional response format. Only empty or json are accepted
languageNoQuery language. Only lynxflow (or omitted) is accepted
curl -s "localhost:3100/api/v1/query?q=from+main+%7C+head+5&limit=5" | jq .

GET /query returns the same JSON envelope and result shapes as POST /query. As with POST /query, format=csv, format=ndjson, and other non-JSON values are rejected.

POST /query/stream​

Stream query results as newline-delimited JSON.

This path is for export and pipeline use, not job management.

Request Body​

POST /query/stream accepts a subset of the POST /query request body:

  • q or query
  • from or earliest
  • to or latest
  • variables
  • language (only lynxflow or omitted)

limit, offset, wait, profile, and format are not silently ignored on this path. If any of them are present, the handler returns 400 VALIDATION_ERROR.

Example​

curl -s localhost:3100/api/v1/query/stream \
-H 'Content-Type: application/json' \
-d '{"q":"from main | where level == \"error\"","from":"-24h"}'

Example response:

{"_time":"2026-02-14T14:52:01Z","_raw":"level=ERROR msg=\"timeout\"","level":"ERROR","message":"timeout"}
{"_time":"2026-02-14T14:51:58Z","_raw":"level=ERROR msg=\"refused\"","level":"ERROR","message":"refused"}
{"__meta":{"total":8432,"scanned":12400000,"took_ms":342}}

Behavior​

  • The response content type is application/x-ndjson.
  • There is no default result limit on this path.
  • The final line is {"__meta": ...} with stream summary data.
  • If a streaming error occurs after output has started, LynxDB writes a final {"__error": ...} line.
  • Client disconnect cancels the streaming query.

GET /query/explain​

Parse and explain a query without executing it.

Query Parameters​

ParameterRequiredDescription
qYes*Query text
queryYes*Alias for q
fromNoStart time bound (accepted for compatibility; does not change the plan text)
toNoEnd time bound (accepted for compatibility; does not change the plan text)
analyzeNoAccepted for compatibility; the LynxFlow explain response does not currently include execution stats
languageNoQuery language. Only lynxflow (or omitted) is accepted

* One of q or query is required.

Valid Response Example​

curl -s "localhost:3100/api/v1/query/explain?q=from%20main%20%22error%22%20%7C%20head%2010" | jq .

The response carries the optimized plan as text in lynxflow_plan, including desugar rewrites and the optimizer rules that fired:

{
"data": {
"is_valid": true,
"lynxflow_plan": "1. Limit(10)\n 2. Filter(contains(_raw, \"error\"))\n 3. Scan(main)\n bloom_term: \"error\"\n\nAnnotations:\n Desugar rewrites:\n search-sugar: \"error\" => where contains(_raw, \"error\")\n Optimizer rules:\n predicate-pushdown (1x)\n",
"errors": []
},
"meta": {
"language": "lynxflow"
}
}

Invalid Response Example​

GET /query/explain still returns 200 OK for invalid queries:

{
"data": {
"is_valid": false,
"errors": [
{
"message": "lynxflow.Explain: parse: unknown stage \"staats\", did you mean \"stats\"?"
}
]
},
"meta": {
"language": "lynxflow"
}
}

GET /query/jobs​

List active and recently completed jobs.

curl -s localhost:3100/api/v1/query/jobs | jq .
{
"data": {
"jobs": [
{
"job_id": "qry_9c1d4e",
"query": "from main | head 5",
"status": "running",
"created_at": "2026-02-14T14:50:00Z"
}
]
}
}

Notes​

  • GET /query/jobs accepts an optional status query parameter. Supported values are running, done, error, canceled, plus aliases complete, failed, and cancelled.
  • Completed, errored, and canceled jobs are garbage-collected after query.job_ttl (default 5m).

GET /query/jobs/{id}​

Fetch a specific job.

Running Job​

{
"data": {
"type": "job",
"job_id": "qry_9c1d4e",
"status": "running",
"query": "from main | head 5",
"progress": {
"phase": "scanning_segments",
"segments_total": 30,
"segments_scanned": 12,
"segments_dispatched": 18,
"segments_skipped_index": 4,
"segments_skipped_time": 2,
"segments_skipped_stats": 1,
"segments_skipped_bloom": 3,
"segments_skipped_range": 0,
"buffered_events": 1200,
"rows_read_so_far": 120000,
"elapsed_ms": 18400
}
}
}

Completed Job​

{
"data": {
"type": "job",
"job_id": "qry_9c1d4e",
"status": "done",
"query": "from main | stats count() by source",
"created_at": "2026-02-14T14:50:00Z",
"completed_at": "2026-02-14T14:50:37Z",
"results": {
"type": "aggregate",
"columns": ["source", "count"],
"rows": [
["nginx", 142847]
],
"total_rows": 5,
"has_more": false
}
},
"meta": {
"took_ms": 37200,
"scanned": 84700000000,
"query_id": "qry_9c1d4e"
}
}

Errored or Canceled Job​

{
"data": {
"type": "job",
"job_id": "qry_d4e1f2",
"status": "error",
"error": {
"code": "QUERY_MEMORY_EXCEEDED",
"message": "query exceeded memory budget"
}
}
}

Canceled jobs use the same envelope shape, with status: "canceled" and an error message such as canceled by user.

Error Responses​

StatusCodeDescription
404NOT_FOUNDJob ID not found

DELETE /query/jobs/{id}​

Cancel a job.

This endpoint requires admin scope when auth is enabled.

curl -X DELETE localhost:3100/api/v1/query/jobs/qry_9c1d4e | jq .
{
"data": {
"job_id": "qry_9c1d4e",
"status": "canceled",
"canceled": true,
"completed_at": "2026-02-14T14:50:37Z"
}
}

canceled reports whether this request actually stopped a running job. If the job had already finished, the endpoint still returns 200 OK, but canceled is false and status reflects the final state.

GET /query/jobs/{id}/stream​

Stream job progress over Server-Sent Events (SSE).

curl -N localhost:3100/api/v1/query/jobs/qry_9c1d4e/stream

Event Types​

EventWhenData
progressAbout once per second while the job is runningProgress information. If preview rows are available, they are embedded in the same event as preview and preview_version
completeJob finished successfullyFinal payload shaped as {data: ..., meta: ...}
failedJob errored{code, message}
canceledJob was canceledProgress snapshot

Example Stream​

event: progress
data: {"phase":"scanning","segments_total":30,"scanned":12,"percent":40,"elapsed_ms":5000}

event: progress
data: {"phase":"scanning","segments_total":30,"scanned":21,"percent":70,"elapsed_ms":9000,"preview":[{"source":"nginx","count":142000}],"preview_version":1}

event: complete
data: {"data":{"type":"aggregate","columns":["source","count"],"rows":[["nginx",284000]],"total_rows":1,"has_more":false},"meta":{"took_ms":10400,"scanned":847000,"stats":{"rows_scanned":847000}}}

The current implementation does not emit a separate partial event type. Preview rows, when present, are attached to progress events.