Let cog_open() cap DuckDB threads (USCOGDATA_DUCKDB_THREADS) #60

Closed
opened 2026-08-09 11:21:17 -04:00 by jared · 0 comments
Owner

cog_open() connects with a bare DBI::dbConnect(duckdb::duckdb()) and sets no threads
pragma, so DuckDB claims every core it can see.

That is right for a single interactive session on a dedicated machine, and wrong for a
server. cog-api now runs two replicas on a host with 8 cores that is shared with other
services and is budgeted 4 for the API. Without a cap, each replica independently claims
all 8 and they fight; with a cap, two replicas fit the budget and a wedged one cannot
starve the box.

Current workaround (please replace it)

cog-api reaches into the namespace at boot, which is not something a consumer should be
doing:

con <- getFromNamespace(".ensure_session", "uscogdata")()
DBI::dbExecute(con, sprintf("SET threads TO %d", n))

It is wrapped in tryCatch and degrades to DuckDB's default rather than failing boot, but
it depends on an internal name and on the session already being open.

Ask

Have cog_open() honour an option/env var — USCOGDATA_DUCKDB_THREADS, following the
existing USCOGDATA_URL precedence (env var > options() > default) — and apply it when
the connection is created. Unset keeps today's behaviour exactly, so this is additive.

memory_limit would be worth the same treatment for the same reason, if it is cheap to
add alongside.

Measured cost of capping

Roughly 5%: a single-government all-years query took 502 ms capped at 2 threads vs 475 ms
uncapped on 16 cores. Cheap enough that a server should always cap, which is the argument
for making it a supported option rather than a workaround.

`cog_open()` connects with a bare `DBI::dbConnect(duckdb::duckdb())` and sets no `threads` pragma, so DuckDB claims every core it can see. That is right for a single interactive session on a dedicated machine, and wrong for a server. cog-api now runs two replicas on a host with 8 cores that is shared with other services and is budgeted 4 for the API. Without a cap, each replica independently claims all 8 and they fight; with a cap, two replicas fit the budget and a wedged one cannot starve the box. ## Current workaround (please replace it) cog-api reaches into the namespace at boot, which is not something a consumer should be doing: ```r con <- getFromNamespace(".ensure_session", "uscogdata")() DBI::dbExecute(con, sprintf("SET threads TO %d", n)) ``` It is wrapped in `tryCatch` and degrades to DuckDB's default rather than failing boot, but it depends on an internal name and on the session already being open. ## Ask Have `cog_open()` honour an option/env var — `USCOGDATA_DUCKDB_THREADS`, following the existing `USCOGDATA_URL` precedence (env var > `options()` > default) — and apply it when the connection is created. Unset keeps today's behaviour exactly, so this is additive. `memory_limit` would be worth the same treatment for the same reason, if it is cheap to add alongside. ## Measured cost of capping Roughly 5%: a single-government all-years query took 502 ms capped at 2 threads vs 475 ms uncapped on 16 cores. Cheap enough that a server should always cap, which is the argument for making it a supported option rather than a workaround.
jared closed this issue 2026-08-10 19:23:15 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Civilytics/uscogdata#60