ClickStack take-home practice

Partitioning

logs is range-partitioned by observed_at, one child table per UTC day. Thirteen relations, one logical table.

13
relations
10
daily partitions
200,000
rows
0
rows in DEFAULT
The logs parent table routing rows into daily partitions and a DEFAULT fallback
INSERT INTO logs never writes to logs. PostgreSQL reads observed_at and routes the row to a child.

Range on observed_at prunes to one child — 483 planning buffers, one partition scanned.

ORDER BY id DESC cannot prune — Merge Append across every child.

01Every partition is an independent table
The parent has no storage of its own. Each child is a real table you can query, VACUUM, or DROP directly — which is exactly why retention is a metadata unlink instead of a bulk DELETE.
02Indexes are per partition, not global
PostgreSQL has no global index across children. Every index in schema.sql is created once per partition, so ORDER BY id DESC becomes a Merge Append over one index scan per child.
03DEFAULT catches whatever matches nothing
A safety net so ingest never fails on a missing daily partition. Rows landing there mean maintenance stopped running, so its count is an alert, not a metric.
SELECT EXISTS (SELECT 1 FROM logs_default);
04Pre-creation has to be somebody's job
This service runs it in-process at startup and hourly. The alternatives: pg_partman for the lifecycle, pg_cron to schedule inside the database, or an external scheduler — a Kubernetes CronJob or an Airflow DAG.

Partition pruning

Pruning is the planner discarding partitions it can prove hold no matching rows. It only works on the partition key observed_at. An id predicate tells the planner nothing about which day a row lives in, so every child stays in the plan.

No bound — what the API does today
SELECT * FROM logs
  ORDER BY id DESC LIMIT 100;
Limit  (actual rows=100)
  -> Merge Append   <- opens every child
    -> Index Scan logs_2026_07_21  rows=1
    -> Index Scan logs_2026_07_22  rows=1
    -> ... 8 more children, one row each
    -> Index Scan logs_2026_07_30  rows=100
    -> Index Scan logs_default     rows=0
children opened
12
planning buffers
2,050 cold
planning time
3.46 / 0.11 ms
execution time
0.175 ms
With an observed_at bound — pruned
SELECT * FROM logs
  WHERE observed_at >= $1 AND observed_at < $2
  ORDER BY id DESC LIMIT 100;
Limit  (actual rows=100)
  -> Index Scan logs_2026_07_30  rows=100
    -> Filter: observed_at >= .. AND < ..

no Merge Append at all —
11 children removed at plan time
children opened
1
planning buffers
486 cold
planning time
1.01 / 0.06 ms
execution time
0.055 ms
Without a bound the planner opens all twelve partitions; with an observed_at bound it opens one and crosses out the rest

Planning dominates — but only on the first query per connection. The 3.46 ms above is a cold catalog cache. Run the same query again on the same connection and planning drops to 0.11 ms (14 buffers, not 2,050). With a pooled connection you pay the cold cost once per backend, then it amortises away. The pruned query still wins warm — 0.06 ms versus 0.11 ms — just by far less than the cold numbers suggest.

How does it scale?O(partitions), flat in rows

Two sweeps, each holding the other variable fixed — otherwise the curve measures both at once. Cost is per child: the planner opens and locks each partition, and Merge Append pulls a first tuple from each. Nothing in either step depends on how many rows a child holds.

Both panels measure cold planning buffers — 8 KB pages the planner reads to build a plan. A page count is deterministic; sub-millisecond timings on this machine are not.

Partitions vary · rows fixed at 64,000
02k4k6k0326496128partitions

Hover for exact values.

Rows vary 1,000× · partitions fixed at 16
02k4k6k9931k99310k995100k9941Mrows in the table

Same y-scale as the left panel.

Table view — every measurement
ConfigurationCold buffersPer partitionCold planExecution
1 partitions1681680.39 ms0.020 ms
2 partitions2231121.43 ms0.072 ms
4 partitions332830.64 ms0.043 ms
8 partitions552691.15 ms0.083 ms
16 partitions993621.92 ms0.149 ms
32 partitions1,875593.26 ms0.311 ms
64 partitions3,640576.23 ms0.437 ms
128 partitions7,1645613.17 ms1.049 ms
1k rows @ 16 partitions9931.84 ms0.103 ms
10k rows @ 16 partitions9931.80 ms0.144 ms
100k rows @ 16 partitions9951.88 ms0.150 ms
1M rows @ 16 partitions9941.97 ms0.170 ms

Linear in partitions. 1 → 128 partitions takes cold planning from 168 to 7,164 buffers with total rows unchanged. That is a steady ~56 buffers per partition across a 128× range. Execution tracks it: 0.020 → 1.049 ms.

Flat in rows. 1,000 → 1,000,000 rows at 16 partitions: planning buffers go 993, 993, 995, 994. A 1,000× data increase moves planning by one page. Execution creeps 0.103 → 0.170 ms — logarithmic, a deeper B-tree descent.

Complexity of an unprunable ORDER BY id DESC LIMIT 100
Grows with…PlanningExecutionWhy
PartitionsO(P)O(P)Every child is opened and locked to plan, then contributes one index scan to Merge Append.
RowsO(1)O(log n)Planning reads catalog, never data. Execution pays one B-tree descent per child and stops at the LIMIT.

So the sizing rule is: partition count is the budget, not table size. At the 30 daily partitions this service retains, an unprunable cursor costs roughly 1,700 planning buffers — invisible. At hourly partitions over the same 30 days that is 720 children, ~40,000 buffers and 70+ ms of cold planning per connection. That is where the observed_at bound stops being optional — and it is a retention-policy decision, not a data-volume one.

How to read the planfield reference
cost=3.34..16007.46
Estimates in arbitrary page-fetch units, not milliseconds. First number is startup cost — work before the first row. Second is total cost for all rows. A LIMIT can stop early, which is why the Limit node's total is tiny while Merge Append's is huge.
rows=200002 width=144
Estimated row count and average row size in bytes. Compare against the actual rows to spot bad statistics — a 100× gap is why a planner picks the wrong join or scan.
actual time=0.117..0.129
Real milliseconds, startup..total, only present with ANALYZE. Times are per loop and inclusive of children, so a parent's time already contains its subtree's.
loops=1
How many times the node ran. Actual time and rows are per loop — multiply by loops for the true total. A nested loop with loops=20000 is where plans usually go wrong.
Buffers: shared hit=36
8 KB pages read from cache (hit) versus disk (read). The honest unit of work — it does not vary with a warm or busy machine the way timings do.
Planning: shared hit=2050
Pages touched to build the plan. This is where partition count bites: the planner opens and locks every child before it can prune. Here planning costs 20× more than execution.
What the observed_at bound looks likecursor design

The fix is not simply a composite cursor. A row comparison alone leaves Index Cond: (ROW(observed_at, id) < ROW(...)) and prunes nothing — the planner needs a scalar predicate, and it needs a floor, since an upper bound only removes partitions newer than the cursor.

  1. 01

    Cursor carries both columns

    nextCursor: `${row.observed_at.toISOString()}_${row.id}`
  2. 02

    Upper bound prunes newer partitions, row comparison keeps paging exact

    WHERE observed_at <= $1
      AND (observed_at, id) < ($1, $2)
    ORDER BY observed_at DESC, id DESC
  3. 03

    A floor is what actually prunes — compute it in JS, never as SQL interval math

    WHERE observed_at >  $3   -- cursor minus one day
      AND observed_at <= $1
      AND (observed_at, id) < ($1, $2)
Cursor shapeChildrenPlan buffersPlanning time
id-only cursor (today)122,1094.05 ms
+ upper bound only91,7193.03 ms
+ 1-day floor, interval math21,7403.53 ms
+ 1-day floor, plain literals26261.29 ms

The last two rows are the surprise: both prune to two children, but writing the floor as observed_at > $1 - interval '1 day' costs nearly three times the planning work of passing a plain timestamp. Compute the window bound in application code.

The row that never arrives

Sorting by id is the right call for paging — it is server-assigned and immune to client clock skew. But id order is allocation order, and rows become visible in commit order. Those are not the same sequence, and a live tail that advances a cursor on id can step over a row that has not committed yet.

Transaction A takes id 5 and B takes id 6, but B commits first; the poller sees id 6, advances its cursor past 5, and never delivers id 5 when A commits

GENERATED ALWAYS AS IDENTITY is a sequence, and sequences deliberately sit outside transaction control — otherwise every insert would serialise on them. The cost of that decision is this gap.

Reproduced deterministically — npm run demo:race

poller cursor starts at 200000

  txn A  INSERT -> id 200005   (uncommitted)
  txn B  INSERT -> id 200006   (uncommitted)
  txn B  COMMIT  -> id 200006 is now visible

  POLL #1: WHERE id > 200000  ->  200006:fast-writer
  cursor advances to 200006

  txn A  COMMIT  -> id 200005 becomes visible, but it is BEHIND the cursor

  POLL #2: WHERE id > 200006  ->  (nothing)
  cursor advances to 200006

  in the table:            200005:slow-writer, 200006:fast-writer
  delivered by the tail:   200006:fast-writer
  never delivered:         200005  <-- the bug

sequence gap: the rolled-back row consumed id 200007, the next committed row got 200008
ids are unique and increasing, but never contiguous.

Is it a bug?

Yes — in the tail. But it is a design limitation, not a coding error: every line does what it intends. The flawed premise is that a sequence cursor tracks visibility order.

It is latent — two ingests must overlap, and a poll must land between one’s id allocation and its commit. And it fails silently: no error, no log, no metric. Sequence gaps are normal anyway (a rollback burns its id), so a missing id can never be used to detect it.

GET /api/logs/tailyesRow is never delivered to any client. Permanent, and silent.
GET /api/logspartlyMissed for that scroll session only; a fresh page one shows it.
GET /api/logs/statsnoAggregates read committed state at query time.
POST /api/logsnoThe write itself is correct and durable.

Shipping it documented was the right call — the correct fix is a substantial build, and the limitation is written down in the decision log rather than discovered by a reviewer. One thing to tighten though: the log calls this acceptable “under the single-writer assumption,” but server/db.ts opens a pool of ten. That assumption was already false — it held because the test traffic was serial, not because the design guaranteed it.

The fix ladder

  1. 1

    Stop the cursor at the first id gaprejected

    The tempting one-liner, and it is wrong. Deletes, rollbacks and dropped partitions all leave permanent holes, so the cursor stalls forever on the first one. Measured: delivered nothing at all.

  2. 2

    Overlap re-read — WHERE id > cursor - Kmitigation

    Narrows the window to whatever K covers and needs dedupe downstream. Never closes it.

  3. 3

    Trailing window — only deliver rows older than Lsimplest fix

    One extra predicate: AND observed_at < now() - L. Correct as long as L exceeds the longest gap between a row's INSERT and its COMMIT, which idle_in_transaction_session_timeout already bounds. Costs L of tail latency and survives id gaps. Measured: delivers the skipped row.

  4. 4

    xmin snapshot watermarkcorrect

    Advance only to pg_snapshot_xmin(pg_current_snapshot()). Same guarantee with no fixed latency penalty, at the cost of real complexity.

  5. 5

    LISTEN / NOTIFY as a wake signalcorrect

    Notifications fire on commit, so allocation order stops mattering. NOTIFY is not durable, so it must trigger a catch-up query rather than carry the payload.

  6. 6

    Logical decoding / CDCcorrect

    The WAL is commit order by definition — and the same pipeline that would feed ClickHouse.

Track B against the prompt

Every row is a literal requirement from the assignment, checked against what the code does — not what the decision log says it does. Two of them do not fully hold.

GET /api/logs/tail — streaming endpoint (e.g. SSE)metValid SSE headers, heartbeat comments every 15s, retry: 3000, one shared process-wide poller on a 1s tick.
…honoring the same filters as the query endpointpartialOnly service and level. The schema is a strictObject, so ?q=timed returns 400 on /tail while /api/logs accepts it. Time bounds are arguably N/A for a tail; q is not.
Clean up when clients disconnectmetclose handler removes the subscriber, and the poll and heartbeat timers stop once the last one leaves.
GET /api/logs/stats — counts by level and by servicemetOne GROUPING SETS query returns totalCount, countByLevel and countByService from a single scan.
…for an optional time rangedeviationfrom/to are optional and default to the last 24h, but any span over 24h is rejected with a 400. A deliberate bound on worst-case cost, not an oversight — say so before they ask.
API-key auth on POST /api/logsmetx-api-key compared with timingSafeEqual after a length check. 503 when unconfigured, 401 on mismatch. Read routes stay public.
…plus per-key rate limitingpartialKeyed on the raw x-api-key header before that header is validated, so rotating it mints a fresh 1,000/min bucket every request. Per-key in shape, bypassable in practice.

Stats — why the range is bounded

Postgres is serving ingest and recent operational reads, not an unbounded OLAP workload. Thirty-day retention helps, but an unbounded from/to would still let one request scan the whole retained month. So the range is optional, defaults to the last 24 hours, and is capped at 24 hours — any wider span is a 400.

The contract, verified

no params        -> last 24h, totalCount 20000
25h span         -> 400 "stats range cannot exceed 24 hours"
exactly 24h      -> 200
to before from   -> 400 "to must be at or after from"
historical day   -> 200, any 24h window in retention

The window is on observed_at, not event_time — so it prunes to one partition and its semantics match retention. It is half-open (>= from AND < to) so adjacent windows tile without double-counting.

One GROUPING SETS query vs two GROUP BYs

QueryBuffersConsistency
GROUPING SETS (one scan)438one snapshot
GROUP BY service438separate snapshot
GROUP BY level79separate snapshot
two queries, combined517two snapshots

Worth correcting yourself on: the decision log says two queries read the pages twice, for half the I/O. Measured here it is 438 vs 517 — about 15%, not 50% — because logs_observed_at_level_idx covers the level aggregate with an index-only scan. The argument that needs no benchmark is the third column: one query, one snapshot, so the counts cannot disagree with each other.

Protecting ingestion — and the hole in it

Started with: hand-rolled key validation, an in-memory limiter, and an api_keys table.

Moved to: express-rate-limit plus one API_KEY environment variable, no table.

Why: removes a database round trip per request and custom limiter maintenance, and gets standard IETF draft-8 headers for free. Deleting your own working code is part of the story, not a detour.

Known limits, stated up front: in-memory counters do not coordinate across instances (needs Redis), they reset on restart, and with one configured key “per-key” is a single shared budget rather than real per-tenant quotas.

The limiter is bypassable. Its key is the raw x-api-key header, read before requireApiKey validates it. Rotate the header and every request lands in a fresh bucket.

rotating key:  r=999   r=999   r=999   <- never decrements
fixed key:     r=999   r=998   r=997   <- correct

The per-IP fallback was reasoned carefully — it stops an anonymous flood from draining a legitimate key’s quota — but it guards the wrong direction. Fix: key on ip + key, or throttle by IP until the key validates.

The live tail — built vs deferred

Straight from the decision log’s own ledger. Only the tracer bullet was marked complete; claiming more than this in an interview is the one unforced error available here.

Follow-up questions

What a ClickHouse engineer is likely to ask about this design, ordered by how probable it is. Answer aloud before opening the spine — reading one is not rehearsing it.

They will pull the ClickHouse thread

near-certain4 questions
01You partition by day in Postgres. What is the ClickHouse equivalent, and is it the same thing?

It is not. PARTITION BY toDate(observed_at) looks identical but does a different job: in ClickHouse the ORDER BY key is the primary index, and partitions are mostly a data-management and TTL unit. Over-partitioning there is a known foot-gun — each partition multiplies parts and merge pressure, where in Postgres it multiplies planner work. Daily is right in both, for different reasons.

02Would you still need the DEFAULT partition?

No. ClickHouse creates partitions implicitly on insert, so there is nothing to pre-create and nothing to fall through to. The DEFAULT partition, the pre-creation job, and the alert on its row count all exist purely because Postgres requires the target partition to exist before the insert.

03What replaces your setInterval retention job?

A declarative TTL observed_at + INTERVAL 30 DAY DELETE on the table. The entire partitions.ts file disappears, along with the advisory lock, the DETACH/ATTACH dance, and the ACCESS EXCLUSIVE stall that comes with it.

04Your id cursor — does it survive the move?

No, and this is the one to volunteer before being asked. A monotonic global sequence is a single-writer construct; there is no IDENTITY across ClickHouse shards. Paging would move to (observed_at, tiebreaker), which changes the public API contract. It is the single design decision in the submission that does not port.

Track B — did you actually meet the spec?

near-certain6 questions
01Show me curl -N 'localhost:5173/api/logs/tail?q=error'.

It returns 400, Unrecognized key q. The tail schema is a strictObject with only service and level, so it rejects a filter the search endpoint accepts. The prompt says the tail should honour the same filters, so this is a genuine gap — matchesFilters is a two-line predicate and adding a substring test on message is small. Time bounds are a fairer omission: from/to on a live tail are close to meaningless, since every row is by definition new.

02What stops me from ignoring your rate limit?

Nothing, today. The limiter keys on the raw x-api-key header and runs before requireApiKey validates it, so rotating the header mints a fresh 1,000-per-minute bucket on every request. I verified it: RateLimit stays at r=999 with a rotating key and counts down correctly with a fixed one. The per-IP fallback was deliberate — it stops an anonymous flood from draining a real key's quota — but it protects the wrong direction. The fix is a composite ip+key, or throttling by IP until the key validates.

03Why cap the stats range at 24 hours when the prompt says the range is optional?

The range is optional — omit both and you get the last 24 hours. The cap is on the span, and it is a deliberate bound on worst-case cost: Postgres is serving ingest and recent reads here, and without a cap one request could scan the entire 30-day retention. Any historical window still works as long as it is 24 hours or narrower. If a product needed month-wide aggregates, the answer is rollup tables or a column store, not lifting the cap.

04Why GROUPING SETS instead of two GROUP BY queries?

One scan and one snapshot, so the level counts and service counts cannot disagree with each other — that argument needs no benchmark. On I/O I would correct my own decision log: it claims two queries read the pages twice for half the I/O, but measured on this schema it is 438 buffers versus 517, roughly 15%, because logs_observed_at_level_idx covers the level aggregate with an index-only scan. The 2× claim holds only without that covering index.

05Your stats window is on observed_at but search filters on event_time. Why the mismatch?

Deliberate, and it is the one place the difference pays off. observed_at is the partition key, so a stats range prunes to a single child — one partition instead of twelve. It also makes the endpoint's time semantics match retention. Search filters on event_time because operators care when the producer says something happened; the cost is that a search time filter can never prune.

06You disconnect slow SSE clients at a 1 MB buffer. Why disconnect rather than drop?

Disconnect is a policy choice: a client that cannot drain 1 MB is not going to catch up on a firehose, and dropping silently gives it a false view of the stream. Reconnecting gets it back to live immediately. The honest caveat is that the decision log lists backpressure as a to-do, so treat this as the shape of the answer rather than a claim that it was fully designed — and any real version needs a metric on how often it fires.

On the skipped row — they will quote your own log line

near-certain7 questions
01You wrote that the live tail may skip a row. Explain it to me.

id comes from a sequence, and sequences allocate outside transaction control so inserts do not serialise on them. That makes id allocation order, while visibility is commit order. Txn A takes id 5, txn B takes 6, B commits first, the poller runs WHERE id > cursor, sees only 6, and advances past 5. When A commits, 5 is behind the cursor and no client ever receives it. It is reproducible on demand — npm run demo:race forces the interleaving.

02So is that a bug you shipped?

In the tail, yes. It is a design limitation rather than a coding error — every line does what it intends; the flawed premise is that a sequence cursor tracks visibility. It is latent (needs concurrent ingest) and silent (no error, no metric). I shipped it documented because the correct fix is a substantial build and the endpoint is explicitly best-effort. What I would tighten is the wording: I called it acceptable under a single-writer assumption, but the pool is ten connections, so that assumption was already false. It held because my test traffic was serial.

03How would you detect this in production if you did not already know about it?

You largely cannot from the data, which is the uncomfortable part. Sequence gaps are normal — a rollback burns its id permanently — so a missing id proves nothing. Detection has to come from the delivery side: count rows the poller emitted versus rows committed in the same window and alert on divergence, or run a reconciliation query that replays a trailing window and compares against what was sent.

04Pick one fix and defend it.

A trailing window, because it is one predicate: AND observed_at < now() - L. It is correct provided L exceeds the longest gap between a row's INSERT and its COMMIT, which idle_in_transaction_session_timeout already bounds, and it costs L of tail latency. I tested it against the forced interleaving and the skipped row is delivered. The tempting alternative — stop the cursor at the first id gap — I also tested, and it is wrong: deletes, rollbacks and dropped partitions all leave permanent holes, so it stalls forever. If the latency mattered I would move to LISTEN/NOTIFY as a wake signal for a catch-up query, since notifications fire on commit.

05Why not just use a timestamp cursor instead of id?

It has the same defect and adds new ones. observed_at is clock_timestamp(), assigned before commit, so a late-committing row can carry an earlier timestamp than one already delivered — identical race. On top of that, timestamps collide, so you need a tiebreaker anyway, and any clock adjustment reorders history. Swapping id for a timestamp changes which column is wrong, not whether the cursor is correct.

06Does the same race affect your keyset pagination?

Yes, but far less severely. A page walks toward lower ids, so a row that commits late above the cursor is skipped for that scroll session and shows up on a fresh page one. That is a consistency artifact rather than data loss. If it mattered, the fix is a repeatable-read snapshot or paging on a snapshot id captured at page one.

07Would this bug exist in ClickHouse?

Not this one — there is no cross-shard sequence to allocate out of order. But the underlying problem does not vanish: inserted parts become queryable after they are committed and merges reorder data, so a tail still needs a watermark rather than an assumption that a monotonic key equals visibility order. Kafka offsets or a CDC stream give commit order directly, which is why real log pipelines put a log in front of the store.

They are an observability company — scale questions

very likely4 questions
01200,000 rows is a toy. What breaks first at 200 billion?

The stats endpoint and free-text search, in that order — both scan rather than seek. At roughly 1M logs/day over 30 days retention you are at 30B rows, well past where a row store on one box is the right call. Ingest and keyset paging would still be fine; aggregation is what forces the move.

02Your q filter is ILIKE '%…%'. What does that cost?

A leading wildcard cannot use a btree, so it is a sequential scan across every partition in range. The Postgres fix is a pg_trgm GIN index per partition. In ClickHouse it is a token bloom-filter skip index or the full-text index. This query is the strongest single argument for moving the workload.

03Why not use ClickHouse from the start?

At the prompt's scale Postgres was correct and every query is bounded. The honest version names the two triggers that would change the answer: p95 latency on the stats endpoint, and search latency once messages stop fitting in cache. Without traffic data, a second datastore is speculative complexity.

04What ORDER BY key would you choose in ClickHouse?

Likely (service, observed_at) — low-cardinality prefix first so the sparse primary index prunes granules effectively, then time within a service. service and level would both be LowCardinality(String), which cuts storage and speeds up the group-bys the stats endpoint runs.

On the measurement work itself

likely3 questions
01Your planning cost was a cold cache. Does that matter in production?

Much less than the raw number suggests. It is paid once per backend connection, so a pool of ten pays it ten times and then it amortises to about 0.11 ms. The pruned query still wins warm, just by 2× rather than 30×. Measuring it is what turned a scary number into a bounded one.

02Why report buffers instead of milliseconds?

A buffer count is a deterministic page count; a sub-millisecond timing on a laptop has an error bar wider than its own value — the warm planning spread at 128 partitions was ±1.5 ms on a 0.76 ms median. Buffers do not move with machine load, so they are the honest unit for a claim about complexity.

03You claim O(partitions). Did you hold rows constant?

Not at first — the initial sweep held rows-per-partition constant, so total rows grew with partition count and the curve measured both variables at once. Rerun with total rows pinned at 64,000, the per-partition cost converges on ~56 buffers and stays flat across a 128× range. The separate row sweep moved planning by one page across 1,000× more data.

The uncomfortable ones

be ready4 questions
01clock_timestamp() splits one batch across two partitions at midnight. Intentional?

Yes. now() freezes at transaction start; clock_timestamp() advances per row, so rows in one batch get distinct observed_at values and a batch landing at UTC midnight can straddle two children. Harmless because reads order by id, not observed_at. now() would also be defensible — one timestamp per batch, cleaner routing — and naming that trade is the actual test.

02Your maintenance job takes ACCESS EXCLUSIVE on the parent to detach DEFAULT. What happens to ingest?

It stalls for the whole detach → create → move → re-attach transaction. This is the sharpest self-identified weakness in the design. The window is short and only fires when a new daily partition is created, and an advisory lock serialises replicas. Production answer: pre-create several days of runway so the pause never coincides with load. DETACH CONCURRENTLY is restricted when a DEFAULT partition exists — which is exactly why pg_partman exists.

03Why does every index on the table exist?

Each one has to name a query. logs_id_idx serves the keyset cursor; logs_service_id_idx and logs_level_id_idx serve filtered paging; the observed_at composites serve stats and the SSE tail. Unused indexes are pure write-amplification, which matters more to a logs product than almost anything else — an index nobody queries is a tax on every insert.

04Two clients connect at the same instant to an idle tail endpoint. Walk me through it.

There is a real race: startStreamTimers guards on pollTimer !== undefined before an await, so two concurrent first-subscribers both pass the guard, both query the latest id, and both call setInterval — the second assignment leaks the first interval. The fix is a promise latch storing the in-flight startup promise rather than the timer. Same check-then-act family as the id-versus-commit-order race already in the decision log.