Background Queries¶
A background query runs a Cypher query out-of-band: Quine Enterprise accepts the query, returns an execution id immediately, and runs the query independently of the request that started it. Nothing blocks on the query finishing, so a query that takes minutes or hours (for example an all-node scan, a bulk update, a large export) is no longer bounded by an HTTP or UI timeout.
Result rows are streamed to destinations, the same set of destinations that standing query outputs use for a query run purely for its side effects. Rows are never stored by Quine Enterprise itself. What is stored is a small status record per execution, whether it started, completed, failed, or was cancelled, how many rows it emitted, and what columns it returned. This is retained until an expiry and then swept.
While an execution runs, you can also watch its rows live over a results tap WebSocket, cancel it, or poll its status.
To run a background query on a recurring schedule rather than once, see Scheduled Jobs.
RBAC Requirement
Running a background query requires GraphRead, plus GraphWrite if the query has write effects and AllNodeScan if it can contain an all-node scan, which are the same permissions the query would need if run directly. Listing, reading status, and tapping results require GraphRead; cancelling and deleting require GraphWrite. See OIDC and RBAC Setup.
When to use a background query¶
Use a background query when the query's results are not what you are waiting for:
- Long-running scans.
MATCH (n) RETURN count(n)over a large graph, or any query that must touch every node. - Bulk mutations. Backfilling a property, relabeling nodes, or repairing data with
Dropas the destination, since there is nothing to collect. - Exports. Streaming a large result set straight into Kafka, S3, or a file without materializing it in a client.
- Scheduled maintenance. The same query, on a schedule. See Scheduled Jobs.
An ordinary interactive query is still the right tool when you want the rows back in the response.
Background does not mean cheap
A background query runs against the same graph as everything else. An all-node scan issued in the background still scans every node and still competes for the same resources. See ID Provider for guidance on querying large graphs efficiently.
Running a query in the background¶
POST the query and its destinations to the graph-scoped background queries endpoint:
curl -X POST "http://localhost:8080/api/v2/graph/quine/backgroundQueries" \
-H 'Content-Type: application/json' \
-d '{
"name": "count-all-nodes",
"query": "MATCH (n) RETURN count(n) AS total",
"destinations": [{"type": "Drop"}]
}'
The response is the execution id, returned as soon as the execution's status record exists, not when the query finishes:
{"id": "6f1c0b3e-9c1a-4f5c-9b0e-1a2b3c4d5e6f"}
Request fields¶
| Field | Required | Description |
|---|---|---|
query |
Yes | The Cypher query to run. Compiled when the request is made, so a query that cannot compile is rejected with a 400 rather than failing later. |
destinations |
Yes | A non-empty list of destinations the result rows are streamed to. Use [{"type": "Drop"}] to discard rows. |
name |
No | A human-readable name, surfaced in the execution record and in the UI. |
parameters |
No | Cypher parameters, as a JSON object. Defaults to {}. |
statusExpiry |
No | How long the status record is retained after the execution terminates, as a duration string such as "1h" or "168h". Defaults to one week. |
Because the endpoint is graph-scoped, the query runs in the graph named in the path. See Namespaces.
At-most-once
A run started this way is at-most-once: if the host executing it restarts or leaves the cluster mid-run, the run is lost and its record is reconciled to interrupted. Scheduled jobs, by contrast, re-fire an interrupted run. See Scheduled Jobs.
Execution status¶
Poll an execution by its id:
curl "http://localhost:8080/api/v2/graph/quine/backgroundQueries/6f1c0b3e-9c1a-4f5c-9b0e-1a2b3c4d5e6f"
{
"id": "6f1c0b3e-9c1a-4f5c-9b0e-1a2b3c4d5e6f",
"jobName": null,
"name": "count-all-nodes",
"query": "MATCH (n) RETURN count(n) AS total",
"status": "completed",
"hostId": "0",
"totalRowCount": 1,
"columns": ["total"],
"error": null,
"expiresAt": "2026-09-08T14:02:11Z"
}
status is one of:
| Status | Meaning |
|---|---|
started |
The execution began and may still be running. |
completed |
The query finished. totalRowCount is the full number of rows emitted and columns the result column names. |
failed |
The query failed terminally. error carries the message. |
cancelled |
The execution was cancelled while in flight. Rows already streamed to destinations are not retracted. |
interrupted |
The executing host restarted or left the cluster mid-run, so the execution ended without recording an outcome. |
jobName is set when the execution was dispatched by a scheduled job; it is null for a run started directly.
Listing executions¶
# Every unexpired execution in this graph
curl "http://localhost:8080/api/v2/graph/quine/backgroundQueries"
# Only executions dispatched by a particular job
curl "http://localhost:8080/api/v2/graph/quine/backgroundQueries?jobName=nightly-count"
Executions are scoped to the graph their query ran against, so this list never shows executions from another graph.
Retention¶
statusExpiry is counted from the moment the execution terminates, not from when it started. A still-running execution therefore stays visible, pollable, and cancellable for as long as it runs, no matter how short its expiry is. Once an execution terminates and its expiry passes, the record is hidden and swept.
Records are removed by expiry, or by an explicit DELETE. There is no other cleanup path: cancelling an execution is a state transition, not a deletion, and a cancelled record expires like any other.
Set statusExpiry longer than you expect the query to run plus however long you want to be able to read the outcome afterwards.
Cancelling an execution¶
curl -X POST "http://localhost:8080/api/v2/graph/quine/backgroundQueries/{id}:cancel"
Cancellation aborts the query stream on whichever cluster member is executing it the request can be sent to any member. The record transitions to cancelled as the executing host unwinds, so the record returned by the cancel call itself may still read started; poll for the transition. Cancelling an already-terminal execution is a no-op.
Destinations that were mid-write see the stream fail. Rows delivered before the cancel are not retracted.
Deleting an execution record¶
curl -X DELETE "http://localhost:8080/api/v2/graph/quine/backgroundQueries/{id}"
Deleting drops the status record immediately instead of waiting for its expiry. If the execution is still running it is cancelled first, then its record is removed. Deleting a record that is already gone returns a 404.
Watching results live¶
Experimental Feature
The results tap is experimental and unstable. Its API and behavior may change without notice.
Each execution exposes a WebSocket that streams its result rows as they are produced:
ws://{host}/api/v2/graph/{graphName}/backgroundQueries/{id}:tap
Use wss:// if your instance is served over TLS.
Each message is a JSON text frame. Row frames are plain objects keyed by result column name. After the run terminates, one final frame is sent under the key __backgroundQueryComplete:
{
"__backgroundQueryComplete": {
"status": "completed",
"totalRowCount": 3000,
"droppedBufferedRows": 0,
"error": null
}
}
Because the completion frame carries the total row count, a consumer can report "showing N of M" even when it did not receive every row.
Buffering and delivery¶
The executing host buffers the first 1024 rows until a subscriber attaches, and retains that buffer until 60 seconds past termination. Connecting to the tap with the id the run endpoint just returned therefore still shows the head of the results, even for a query that finishes before your client connects. Rows beyond the buffer that are produced before anyone attaches are counted in droppedBufferedRows.
Once a subscriber attaches, the buffer flushes in order and subsequent rows stream live.
The tap is best-effort and has no delivery guarantees. It never applies backpressure to the query: a slow consumer drops frames rather than slowing the run down. Reconcile against totalRowCount in the status record or in the completion frame when the exact count matters.
A run cancelled before any subscriber ever attached discards its buffer entirely; cancelling disclaims the results.
Cluster behavior¶
You can connect the tap to any cluster member and receive the rows produced by the member actually executing the query. You do not need to know which member the run landed on.
Example¶
Start a run that emits a burst of rows, then watch it:
ID=$(curl -s -X POST "http://localhost:8080/api/v2/graph/quine/backgroundQueries" \
-H 'Content-Type: application/json' \
-d '{"query": "UNWIND range(1, 3000) AS i RETURN i", "destinations": [{"type": "Drop"}]}' \
| jq -r '.id')
websocat "ws://localhost:8080/api/v2/graph/quine/backgroundQueries/$ID:tap"
{"i": 1}
{"i": 2}
{"i": 3}
...
{"__backgroundQueryComplete": {"status": "completed", "totalRowCount": 3000, "droppedBufferedRows": 0, "error": null}}
Streaming results to a destination¶
To keep the results, give the query a real destination instead of Drop. Destinations use the same configuration as standing query outputs:
curl -X POST "http://localhost:8080/api/v2/graph/quine/backgroundQueries" \
-H 'Content-Type: application/json' \
-d '{
"name": "export-accounts",
"query": "MATCH (a:Account) RETURN a.id AS id, a.balance AS balance",
"destinations": [
{
"type": "Kafka",
"topic": "account-export",
"bootstrapServers": "localhost:9092",
"format": {"type": "JSON"}
}
]
}'
More than one destination can be given, and every row is delivered to all of them. See Standing Queries for the full set of destination types and their options.
Using the UI¶
Background queries can be started and watched from the browser without writing any HTTP calls:
- The Streams page has a Background Queries panel that lists the graph's runs, creates new ones, and inspects results as they arrive. See Streams.
- The Exploration UI query menu has a Run in background action that dispatches the query bar's contents. See Exploration UI.
API reference¶
| Operation | Endpoint |
|---|---|
| Run a background query | POST /api/v2/graph/{graphName}/backgroundQueries |
| List background queries | GET /api/v2/graph/{graphName}/backgroundQueries |
| Get background query status | GET /api/v2/graph/{graphName}/backgroundQueries/{id} |
| Cancel a background query | POST /api/v2/graph/{graphName}/backgroundQueries/{id}:cancel |
| Delete a background query | DELETE /api/v2/graph/{graphName}/backgroundQueries/{id} |
| Background query results tap | GET /api/v2/graph/{graphName}/backgroundQueries/{id}:tap (WebSocket) |
Next steps¶
- Scheduled Jobs: run a background query on a recurring schedule.
- Standing Queries: destination types, formats, and result enrichment.
- Standing Query Wiretap: the equivalent live view onto a standing query's output workflow.