Build1 distinct publisher3 min readUpdated
A read-only GraphQL service over an existing SQLite file cut a four-request search screen down to one. The interesting parts are DataLoader, cost limits and a query_only pragma.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A search screen on DailyWatch's Android client used to need four HTTP requests to draw one list: a search call, a channels-by-ids call, a categories call, then a per-video call as soon as the user tapped a card [1]. The fix the author landed was not a rewrite but a read-only GraphQL sidecar in Strawberry and FastAPI opened against the same SQLite file the existing PHP site reads [2]. The numbers are the argument. Every one of those REST endpoints returned the full row, 41 columns for a video, of which the client rendered six [3]. On a throttled 3G profile that screen took 2.4 s at p95 to first meaningful paint and moved roughly 180 KB of JSON to draw something the author puts at 20 KB of actual need [4]. That is about nine times the payload the UI consumes [5]. Latency chaining did the rest: request two could not start until request one returned the channel IDs, so two serial round trips on a 250 ms mobile connection burned half a second before any pixel moved [6]. The failure was composition, not endpoints. The author is explicit that each endpoint was individually fine [7], and that /api/channels?ids=a,b,c was a hand-rolled DataLoader with no schema, no batching contract, and a URL length limit [8]. Four responses also meant four TTLs and four invalidation paths, and when the cron job refreshed trending videos one of the four consistently went stale in a way that made the UI look broken [9]. Adding a field to the search response had already broken an older client doing strict JSON decoding, with no way to tell which fields were in use [10]. Note what did not change. The site is a PHP 8.4 monolith behind LiteSpeed with a page cache, server-rendered, no hydration, SQLite reads in single-digit milliseconds, and the author says none of it needed touching [11]. The apps were paying for a data model shaped around HTML pages [12]. The reason to put GraphQL beside the monolith rather than inside it is operational. Because the read model is a file, a second process can open the same database read-only in WAL mode with no replica, no pool, and no network hop to the data [13]. If the sidecar dies the website keeps serving HTML and only the apps degrade, to a cached view; a GraphQL layer inside the monolith would share a fatal error handler with the revenue path [14]. Strawberry being code-first means pyright catches a resolver returning the wrong shape at build time, where an SDL file drifts from its resolvers inside about two sprints [15]. And the access pattern is many small indexed reads rather than one large query, which batches well [16]. The author also prices the sidecar honestly: one more deploy target, one more origin behind Cloudflare, one more thing to monitor, and a hard invariant that the service never writes, enforced with PRAGMA query_only = 1 on every connection rather than trust [17]. That pragma is the load-bearing line in the whole design. The remaining work listed is the unglamorous part and the reason to read the piece: schema design against an FTS5 index, keyset pagination over bm25 scores, killing the N+1 with DataLoader, cost limits, and making a POST-shaped protocol cacheable at the CDN edge [18]. One detail shows how far the shared surface has to reach. Full-text query construction had to be shared rather than forked, and the existing PHP sanitiser strips anything that is not a letter, number or whitespace before capping input at eight tokens, because FTS5 treats characters like " * : ^ - AND OR NOT as syntax while users type them as text [19].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The DailyWatch Android client painted one search screen with four HTTP requests: GET /api/search?q=, then GET /api/channels?ids=, then GET /api/categories, then a per-video GET /api/video/{id} when the user tapped a card.
The fix was a read-only GraphQL sidecar built with Strawberry and FastAPI, pointed at the same SQLite file the PHP site already reads.
Every one of those endpoints returned the whole row out of SQLite, 41 columns for a video, and the client rendered six of them.
On a throttled 3G profile the screen took 2.4 s at p95 to first meaningful paint, and about 180 KB of JSON crossed the wire to draw something that genuinely needs 20 KB.
Request 2 cannot start until request 1 returns the channel IDs; two serial round trips on a 250 ms mobile connection is half a second before any pixel moves.
The author says the endpoints were individually fine; the composition was the problem.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed but wholly first-party and one-sided
The mechanism is documented well above blog-average: real endpoint shapes, a working PHP FtsQuery listing with bm25 weights and MAX_TOKENS, an explicit query_only pragma, and quantified before-state costs. But every figure is self-reported by the engineer who chose the architecture, from a single dev.to item with no methodology, no post-migration measurement, and no second source. The supplied body is also truncated before the promised pagination, cost-limit and CDN sections, and one load-bearing rationale (SDL drift) is pure assertion.
One first-party production deployment, no external uptake
Adoption evidence amounts to a single self-disclosed deployment at the author's own property, DailyWatch: a read-only Strawberry/FastAPI sidecar running as an extra Cloudflare origin over an existing SQLite file. There is no user count, request volume, second team, downstream reuse, or third-party report of the pattern in the supplied material, and no post-change performance disclosure to show the deployment achieved its aim.
Close to aligned, with mild single-case generalisation
The framing is notably restrained for the genre: the author names the monolith as already fast and unchanged, itemises the sidecar's overheads (extra deploy target, extra origin, extra monitoring), and mechanically enforces the read-only invariant instead of claiming discipline. The small positive gap comes from presenting one unaudited before-state as settled measurement, from advertising results (pagination, cost limits, CDN caching, four-calls-to-one) that the supplied text does not yet demonstrate, and from generalising the SDL-drift claim beyond any evidence.
Author advocating own architecture; no vendor interest disclosed
The visible incentive is ordinary practitioner reputation: the author is the engineer who selected the sidecar pattern and is publishing the case for it on a developer platform, so favourable framing of the chosen design is expected. Nothing in the supplied source indicates sponsorship, vendor employment at Strawberry, FastAPI, SQLite, LiteSpeed or Cloudflare, a commercial product being sold, or a funding or licensing angle, and the candid cost list works against pure promotion.
Moderate-low: coherent single account, no corroboration
The internal account is coherent and unusually concrete, which supports confidence in what the author did. Confidence is capped by structural limits: one publisher, one item, one first-party voice, self-reported metrics without method, a truncated body that omits several promised sections, and zero post-migration outcome data. Directional confidence in the pattern's plausibility is higher than confidence in any specific number.
build
Once the question needs a cube, you own the parser1 distinct publisher
build
Stop timing your GraphQL tests and start counting loader calls1 distinct publisher
build
trelix's most useful release detail is an exit code, not the audit log1 distinct publisher
build
Force the tool call, then hand Lightsail a long-lived key1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 17, 2026