LinkedQL definitely optimizes at multiple levels between a change happening on your database and the live result your application sees. The most significant of these being its concept of query windows and query inheritance which ensure multiple overlapping queries converge on a single "actual" query window under the hood.
You want to see the engineering paper for the full details: https://linked-ql.netlify.app/engineering/realtime-engine
Of course, it comes at the cost of some stability. However I was just curious if such an abstraction could support such use cases. Thank you for the link to the paper!
And of course achieving that "exceptionally high throughput and performance" is the ultimate goal for a system of this nature.
Now, yes — LinkedQL reasons explicitly in terms of transactions, end-to-end, as covered in the paper.
The key structural distinction is that LinkedQL does not have the concept of its own transactions, "since it doesn’t initiate writes". Instead, it acts as an event-processing pipeline that sits downstream of your database — with a strict "transaction-through rule" enforced across the pipeline.
What that transactional guarantee means in practice is this:
Incoming database transactions (via WAL/binlog) are treated as "atomic" units. All events produced by a single database transaction are received, processed, and propagated through the pipeline with their transactional grouping preserved, all the way to the output stream.
Another way to think about it:
You perform high-throughput writes (multi-statement transactions, bulk writes, stored procedures, batching, etc.)
→ LinkedQL receives the resulting batch of mutation events from that transaction
→ processes that batch as "one" atomic unit
→ emits it downstream as "one" atomic unit
→ observers bound to the view see a "single" state transition composed of many changes, rather than "a flurry" of intermediate transitions.
Effectively, a systems that thinks in terms of batching and other throughput-oriented write patterns. LinkedQL just doesn’t initiate its own transactions — it preserves yours, end-to-end.As a potential user, I'd probably be thinking through things like: if I have a ~small-fleet of 10 ECS tasks serving my REST/API endpoints, would I run `client.query`s on these same machines, or would it be better to have a dedicated pool of "live query" machines that are separate from most API serving, so that maybe I get more overlap of inherited queries.
...also I think there is a limit on WAL slots? Or at least I'd probably want not each of my API servers to be consuming their own WAL slots.
Totally makes sense this is all "things you worry about later" (where later might be now-/soon-ish) given the infra/core concepts you've got working now -- looking really amazing!
First, I’m implicitly assuming your 10 ECS tasks are talking to the same Postgres instance and may issue overlapping queries. Once that’s the case, WAL slots and backend orchestration naturally enter the story — not just querying.
A few concrete facts first.
PostgreSQL caps logical replication slots via `max_replication_slots`. Each LinkedQL Live Query engine instance uses one slot.
Whether “10 instances” is a problem depends entirely on your Postgres config and workload specifics. I’d expect 10 to be fine in many setups — but not universally. It really does depend.
---
That said, if you want strong deduplication across services, the pattern I’d recommend is centralizing queries in a separate service.
One service owns the LinkedQL engine and the replication slot. Other backend services query that service instead of Postgres directly.
Conceptually:
[API services] → [Live Query service (LinkedQL)] → Postgres
From the caller’s point of view this works like a REST API server (e.g. `GET /users?...`), but it doesn’t have to be "just" REST.
If your technology stack requirements allow, the orchestration can get more interesting. We built a backend framework called Webflo that’s designed specifically for long-lived request connections and cross-runtime reactivity — and it fits this use case very naturally.
In the query-hosting service, you install Webflo as your backend framework, define routes by exposing request-handling functions, and have these functions simply return LinkedQL's live result rows as-is:
// the root "/" route
export default async function(event, next) {
if (next.stepname) return next();
const q = event.url.q;
const liveResult = await client.query(q, {
live: true,
signal: event.signal
});
// Send the initial rows and keep the request open
event.respondWith(liveResult.rows, { done: false });
}
Here, the handler starts a live query and returns the live result rows issued by LinkedQL as "live" response. * The client immediately receives the initial query result
* The HTTP connection stays open
* Mutations to the sent object are synced automatically over the wire and the client-side copy continues to behave as a live object
* If the client disconnects, event.signal is aborted and the live query shuts down
On the client side, you'd do: const response = await fetch('db-service/users?q=...');
const liveResponse = await LiveResponse.from(response);
// A normal JS array — but a live one
console.log(liveResponse.body);
Observer.observe(liveResponse.body, mutations => {
console.log(mutations);
});
// Closing the connection tears down the live query upstream
liveResponse.background.close();
There’s no separate realtime API to plumb manually, no explicit WebSocket setup, and no subscription lifecycle to manage. The lifetime of the live query is simply the lifetime of the request connection.---
In this setup:
* WAL consumption stays bounded
* live queries are deduped centrally
* API services remain stateless
* lifecycle is automatic, not manually managed
I haven’t personally run this exact topology at scale yet, but it fits the model cleanly and is very much the direction the architecture is designed to support.Once you use Webflo, this stops feeling like “realtime plumbing” and starts feeling like normal request/response — just with live mode.