Sign in

Platform

GET /api/v1/stats against the public corpus returns 14,569 binaries — and two engine versions serving side by side:

"analyze_status":  {"analyzed": 14320, "empty": 249},
"engine_version":  {"0.10.3": 12640, "0.11.0": 1929},
"linkage":         {"object": 12676, "dynamic": 1876, "static": 17},
"verdict":         {"benign": 13805, "suspicious": 324,
                    "not_enough_evidence": 319,
                    "likely_malicious": 101, "malicious": 20}

Every row of that answer is stored analysis output. The platform indexes what the engine produced, serves it over two surfaces, and analyzes nothing in-process. The second surface is an MCP server carrying the same bearer tokens through the same gate code, so an HTTP route and its MCP twin cannot drift on who is allowed to call it.

SectionWhat it answers
Analysis addressing and reanalysishow two engine versions serve at once, and what a reanalysis costs
Resolved call-site searchhow to search resolved behavior — which binary opens which path — rather than text
The query DSLwhich fields are filterable, which operators each accepts, what the grammar refuses
Search rankinghow field boosts and popularity order full-text results
Similaritywhy the surfaced score is Jaccard rather than cosine, and how neighbors are retrieved
The ML layerthree optional ONNX models, one native-Rust reference, and what a deployment without model files serves
Visibility scopeshow one SHA-256 holds several independent analyses, and why invisible means 404
Feature gatingwhich capabilities a plan can buy, which only a per-user grant opens, and which status code each refusal returns
Sessions and API keyscredential formats, lifetimes, scopes, and what a wrong key costs to reject
Ingestwhich blobs become analysis jobs, why the engine runs in a child, how a job reports life
API routesthe surface, the per-route caps, and the gating
Index schema pinswhat forces a reindex, and what breaks the build instead of corrupting scores
Measured costlatency and memory at 100K and 1M binaries, and which numbers measured a since-fixed defect
Scope and non-goalswhat the platform does not do

Analysis addressing and reanalysis

Analysis output is addressed by the triple (visibility scope, sha256, engine version), so shipping a new engine adds an address rather than invalidating a corpus. Neither version above is stale.

POST /api/v1/binary/{sha256}/reanalyse fills an address on demand, reusing the bytes already stored:

ResponseConditionCompute spent
already_currentthe stored engine version is at or above the deployment'snone
in_progressa job for those same bytes is already runningnone additional
202 pendingneither of the aboveone analysis job; the response carries a job id

Call sites the engine resolved are indexed as facts, not text — which binary opens which path, execs which target, loads which library. GET /api/v1/search/signatures?path_prefix=/etc on the live corpus:

{"name": "chpasswd",  "op_name": "access", "aspect": "file_io",
 "args_preview": "{\"path\":\"/etc/shadow\"}",
 "call_site": {"func_name": "main", "func_addr": 4624, "offset": 2222}}

{"name": "adjkerntz", "op_name": "access", "aspect": "file_io",
 "args_preview": "{\"path\":\"/etc/wall_cmos_clock\"}",
 "call_site": {"func_name": "sub_2020", "func_addr": 8224, "offset": 167}}

Each row names the containing function and a byte offset within it, so a hit is an address to open rather than a keyword to chase. That offset is block-granular, not per-instruction — the lifted IL does not carry a virtual address per operation — so it lands you in the right basic block and an accompanying operation index picks the operation out of it.

Path normalization. Paths are normalized before indexing, which is what makes them comparable across machines:

Path shapeIndexed asWhy
One of 19 system prefixesverbatim, short-circuiting every rule belowthe path is the same on every machine, so collapsing it would destroy signal
/Users/…//Users/*/…the user name differs per machine
/home/…/collapsed identicallyas above
C:\Users\…\collapsed identically, case-insensitivelyas above
/var/folders/…/…/collapsedDarwin per-process temp
/root/passes through unchanged, deliberatelya UID-0 home directory identifies nothing

So /Users/alice/Library/Keychains/login.keychain-db indexes as /Users/*/Library/Keychains/login.keychain-db. The un-normalized original stays inside the stored analysis, so the specific observed path is never lost.

The 19 preserved prefixes span all three platforms:

PlatformPreserved prefixes
macOS/System/Library/, /Library/Apple/, /Library/Application Support/, /usr/libexec/, /private/var/db/, /private/tmp/
WindowsC:\Windows\, C:\Program Files\, C:\Program Files (x86)\, C:\ProgramData\
Linux and BSD/etc/, /usr/bin/, /usr/sbin/, /usr/lib/, /usr/local/, /var/log/, /proc/, /sys/, /tmp/

One list serves both the analysis-time writer and the query-time normalizer, so the two cannot drift; path_prefix=/etc above works because /etc/ is on it.

Limits.

The query DSL

71 field identifiers are accepted, each carrying a kind that fixes which operators are legal on it. Anything else raises unknown identifier.

KindOperatorsFields include
Single-value facet== !=signing_status, binary_type, platform, arch
Multi-value facet== != ANY ALL CONTAINSindicator_categories, attack_techniques, component, package, vuln_severity
Boolean facet== !=has_kev, is_apple_signed, action_opens_network, action_likely_packed
Tokenized text== CONTAINS ANYframeworks, entitlements, imported_symbols, bsd_syscalls, xpc_services
Numeric== != > >= < <=text_entropy, max_section_entropy, function_count, known_vuln_count
Exact string== !=sha256, cdhash, team_id, symbol_hash, syscallhash

The DSL rides /api/v1/search as filter=; there is no separate query route. A real request and its real answer:

GET /api/v1/search?filter=component == "libcurl" AND NOT is_apple_signed == true

curl            239,016 B   02c1abdd9468
librepo.so.0    227,864 B   00f8536eda1a
curl            334,312 B   0513ba358cab
total: 3

Hierarchical values. component and package are hierarchical — the same value string, cut at a different depth:

FilterMatches
package == "macos"every macOS-kind package
package == "macos/macos"every release
package == "macos/macos/26.4"exactly one

Grammar limits.

Search ranking

Full-text scoring is BM25 with per-field boosts, because a token in a filename means something different from the same token in __cstring.

FieldBoost
name20.0
Frameworks3.0
Entitlements, XPC services2.0
ObjC selectors, API-call lists0.5
Extracted strings0.3
Query equal to a binary's entire lowercased filename50× (a separate clause)

That exact-name clause is why an exact name cannot be buried by partial-token hits. The spread it produces, measured on the corpus for the query curl:

curl                 780.16   matched: filename, apis, strings
curl                 679.10   matched: filename, apis, strings
libcurl.so.4.7.0       8.24   matched: apis, strings
ps                     4.09   matched: apis

Two orders of magnitude between the program named curl and the library it links. Without the boosts, a high-cardinality content field accumulates enough term matches to bury the exact-name hit.

Popularity then re-ranks: bm25 × (1 + α × popularity_norm), where popularity is w_u · ln1p(uploads_30d) + w_l · ln1p(lookups_30d) over a sparse per-day table holding only the trailing 30 days, pruned on the next write. The logarithm is load-bearing — raw counts let a single viral binary dominate every query.

Similarity

The score GET /api/v1/similar/{sha256} surfaces is Jaccard, not cosine, because cosine cannot separate unrelated programs. On a Linux curl — one real match and a run of near-misses — both scores it computes:

neighbor            score   cosine   jaccard
curl                0.710    0.987     0.710
libdpv.so.3         0.147    0.985     0.147
bsdtar              0.138    0.980     0.138
ssh-agent           0.062    0.971     0.062
scp                 0.053    0.982     0.053

Cosine puts another curl and an scp within 0.005 of each other, because it measures count magnitude: any two non-trivial programs read as "about the same size" and land above 0.97. Jaccard separates that same pair by 13×.

ModeScoreResult
defaultweighted Jaccard over the reranked candidate poolthe score surfaced above
mode=vectorcosine over the 106-dimension vectorexplicit opt-in; makes the failure explicit — the top-ranked neighbor of curl becomes gpgv2 at 0.995
mode=combined0.4 × cosine + 0.6 × jaccardexplicit opt-in; the blend read unrelated binaries as roughly 55% similar

The DNA vector

The vector behind that cosine column has 106 dimensions and is stored padded to 112: 106 is divisible by neither 8 nor 16, and an IVF_PQ index wants a divisible width for its sub-quantizers. The pad is zeros, so cosine distance is unchanged, and it is stripped on read.

Two-phase retrieval

Retrieval is two phases: an approximate nearest-neighbor scan for candidates, then a weighted-Jaccard rerank of the top 500 of them.

Phase one is an approximate nearest-neighbor query with two predicates pushed into the index rather than applied after: the caller's visibility set, and the target's own file format. Format scoping is not an optimization — roughly 22 of the 106 dimensions and several of the content sets are Apple/Mach-O-only, so a cross-format score is not comparable in the first place.

Phase two takes the top 500 candidates and re-ranks them with a weighted Jaccard over 40 content sets (the stored blob carries 41 sets; one is persisted for export without carrying a rerank weight):

Content setWeight
Entitlements, matched indicators, resolved paths, exec targets6.0
ATT&CK techniques, XPC services5.0
Static libraries3.0
ObjC class names0.5

Those weights are the outer prior in both scoring modes; what happens inside one set depends on corpus size:

CorpusOverlap within a setWhy
Under 1,000 binariesplain count Jaccarddocument frequencies over a corpus that small are too noisy to trust
1,000 and aboveevery member scaled by its corpus inverse document frequency; a set whose members are all corpus-ubiquitous drops out of the average rather than diluting ita member present in nearly every binary then contributes almost nothing to either the intersection or the union

The 14,569-binary corpus this brief opens with is always on the frequency-weighted path.

The pool is 500 rather than something tighter because cosine and Jaccard order differently — the curl neighbor list opening Similarity is that divergence — and a small pool drops true Jaccard-top matches before the rerank ever sees them.

The learned embedding

A 128-dimension learned embedding lives in the same vector store as a nullable second column, so the two rankings can be compared on the same corpus. A config flag routes /similar to it without touching the DNA path. Its scoring is cosine-only with no Jaccard rerank; candidate rows whose embedding column is null are invisible to it, and a target with no stored embedding falls back to the DNA path rather than returning nothing.

The ML layer

No trained model ships with the software, so a default deployment runs the null path and returns no predictions at all. Model files are operator-supplied paths pinned by sha256 in configuration.

Three of the four predictors are ONNX-served, and all three are fail-soft by construction: each ships a null implementation alongside the real one, and a missing model file, a missing ONNX Runtime dylib, or a prediction error degrades to serving without predictions rather than failing the request. The fourth needs no runtime dylib at all.

ModelBackingWhat it computesThreshold to run
Binary-type classifierONNX11 classes over the structural vector; outputs one of kext, dylib, bundle, UI app, menu-bar app, XPC service, helper tool, daemon, agent, CLI tool, other
Per-cohort anomaly scoringONNXone isolation forest per binary type, so a daemon is scored against other daemons rather than against the corpus; outputs a raw score, a percentile read off a precomputed 101-entry cutoff table (one raw-score threshold per percentile, binary-searched at request time), and a flag raised at the 95ththe type classifier at least 0.8 confident in its label
Behavioral embeddingONNXthe 128-dimension learned vector described above, over the same DNA feature vector, emitting an L2-normalized embedding
Function-rarity referencenative Rustper-type k-means centroids plus a regularized pseudo-inverse covariance, retrained from the corpus in-processat least 50 function vectors for that binary type

Both thresholds decline rather than guess. Below 0.8 confidence a binary is not scored rather than scored against the wrong cohort, and binary types under 50 function vectors are skipped entirely. The classifier's label set is macOS-shaped, which is a real constraint on what the downstream cohorts can mean for an ELF or PE.

Visibility scopes

Every identity carries one of /public, /private/<user id>, or /workspace/<workspace id>, and the same string serves as the Tantivy facet, the vector-store filter predicate, and the on-disk directory segment.

One SHA-256 can hold several at once: the same binary uploaded into two workspaces is one blob with two membership rows and two independent analysis subtrees. Because the analysis subtrees are independent, a takedown against the public copy leaves the private and workspace analyses intact.

A binary the caller cannot see returns 404, not 403 — on the detail route, on compare, on strings, on reanalyse. Invisible and absent are indistinguishable from outside, so a private binary's existence never leaks through a status code.

Feature gating

Two different refusals live behind the word "gated", and they leak different things.

RefusalStatusWhat the caller learns
Registry-gated capability the caller does not hold404nothing — the route reads as nonexistent
Pro capability with an upsell path (compare, full strings)403, with a typed codethat the capability exists and costs money
Binary the caller cannot see404nothing
Read-only API key on a mutating route403, after the visibility checknothing about existence — the 404 fires first

Compare and full strings take the 403 path deliberately: a capability nobody can see is a capability nobody upgrades to.

Five capabilities no plan can buy. BinaryDownload, whole-corpus global statistics, curator annotation, whole-binary decompilation, and the static-audit surface (CWE findings and secrets) return false for every tier — Free, Pro, and Enterprise alike. Access comes solely from a per-user grant issued by the operator; static audit reached that state by demotion from Pro.

A deployment with no auth provider configured auto-grants all five to whoever is running it, because a self-hosted install must not lock out its own operator. With auth configured, a real grant is required.

Sessions and API keys

Sessions are HS256 JWTs with a single-algorithm allowlist (alg: none and every asymmetric algorithm are refused), an explicitly re-set required-claims list so a library upgrade cannot silently relax it, and zero clock leeway so an expired token is expired.

CredentialFormLifetime
SessionHS256 JWT24 hours (default)
Workspace invitetoken7-day TTL
API keyob_key_ plus 32 random bytes, base64url-encoded; issued per user, capped at 10 active per user, optionally scoped to one workspace and independently marked read-only or writeno expiry

The four key-management routes are session-only, so a leaked CI key cannot mint peers, list them, or revoke them. Usage timestamps are debounced to one write per minute per key rather than one per request.

Key lookup is asymmetric: a key present in the hash index resolves in 1.3 µs at 100,000 issued keys, while a key that misses the index falls back to a linear scan over every active key.

Active keys issuedHit (hash index)Miss (linear scan)
1,0002.4 µs
10,00025 µs
100,0001.3 µs732 µs

Presenting a wrong key costs 550× more than presenting a right one, and that cost grows with the number of right ones.

The scan is a degradation, not the design: the hash index is backfilled at boot from the stored keys. If that backfill fails, every request — right key or wrong — pays the 732 µs path, which is why the failure is logged rather than swallowed. Revoked keys stay in the index and are re-checked for active status on every hit, so revocation is not an index delete.

Ingest

Upload routing goes through the same signature authority the unpacker dispatches extractors from, so the front door and the engine can never disagree about what a blob is.

The authority calls itFormatsWhere it goes
AnalyzableMach-O, ELF, PE/COFFan analysis job
Extractablearchives, single-stream compression, filesystems, firmware and disk carriersthe packages tier for unpacking, regardless of size
Inert leaf contentimages, documents, fonts, capturesrefused by name
In-binary evidence contentkey material, crypto tables, encrypted-firmware envelopes with no descent pathrefused by name — these are things found inside a binary, not things to upload
Scriptanything opening with a shebangrefused by name
Unrecognizedstored and scanned optimistically, with the authoritative full-buffer scan running inside the unpack job, unless the door test refuses it

The door test is narrower than "is this noise". A blob is refused if it is under 1,024 bytes — the firmware this gate must accept is megabytes — or if 95% or more of its first 8,192 bytes are printable ASCII. That second rule refuses logs, configuration files, and source code: not noise, simply not containers, and each would otherwise buy a full unpack job for a text file.

A firmware image already unpacked short-circuits on re-upload, because a completed unpack always leaves an SBOM sidecar. A second upload of the same bytes therefore does not re-run a multi-minute unpack costing one of the few concurrent permits and up to 16 GiB of disk churn. The uploader is still recorded against the existing package and any child missing an analysis is backfilled. Re-running for real is an explicit POST /package/{sha256}/re-extract.

Out-of-process analysis

Analysis runs out-of-process, and the process boundary is the point: the engine parses attacker-controlled input, so a panic, stack overflow, or parser memory bug kills one child and nothing else.

The cost of that boundary is the cold start. FLIRT signatures, bundled PE pattern files, and tier-3 symbols all live in per-process caches that die with the process. How much that costs depends on which warming path a deployment is on:

Warming pathCost per workerWhat it holds
Shared flat signature artifact present — the normal path, built by the server before the pool spawnsa memory map, effectively instantone physical copy shared across every worker through the page cache
No flat artifact — the fallbackabout 1 s for the ~1.2M-signature FLIRT corpus plus about 6 s for the bundled PE pattern text parsea private multi-hundred-megabyte parsed heap per worker

The tier-3 local symbol database, at roughly 34,000 entries, has no flat artifact and always warms by loading; it is small enough not to matter. A pool of long-lived engine children pays whichever cost applies once — each worker warms at server startup, and the pool reports ready only when every worker is warm, so the first upload lands on a hot fleet. A worker that fails to warm within 120 s is treated as failed.

Isolation survives the reuse. A worker leaves the pool on any of five conditions:

Three independent guards stop children outliving the server, including PR_SET_PDEATHSIG on Linux, which survives a SIGKILL of the parent where no cleanup code runs at all.

Remote workers can additionally claim jobs over a token-gated compute plane, purely additive to the local pool. That plane is gated by its own deployment token — not user auth, and distinct from the operator token — and returns 404 on every route when the token is unset, so its existence does not leak.

Jobs, progress, and liveness

Three job types exist: analysis, reconstruction, and package unpack. Progress streams over Server-Sent Events with a stage label and a percentage; a firmware unpack emits unpacking, then the unpacker's own stage names as it moves through them (pre-scan, per-format labels, overview, scanning, finalizing), then firmware_overview, then one child_extracted per extracted child, then complete.

child_extracted is not one event per binary. The same stage label carries three payload kinds:

PayloadRouted to
Extracted binaryan analysis job
Extracted source file (.js, .py, and similar)source-level static analysis
Static-library archivethe signature-minting store, and only when the source package is curator-owned and public, because it mints a labeled library signature rather than an analysis

Liveness is tracked separately from progress, because a job that produces nothing and a job that has died look identical without a clock. The clocks belong to different jobs:

ClockThresholdScopeEffect
Host heartbeatevery 5 sall jobsthe liveness signal itself
Subprocess liveness tickevery 20 spackage unpackkeeps the freshness timestamp moving from inside the child
Stalled-job threshold120 s idlepackage unpackthe job is reported stalled — an operator warning, not a kill
Silence watchdog300 s with no event, after a 30 s startup grace for the legitimately quiet input fetch and pre-scanpackage unpack subprocessthe subprocess is killed
Hard wall-clock ceiling1,800 spackage unpack subprocessthe backstop for a child that is alive but wedged, and so never trips the silence watchdog
Analysis deadline300 s base plus 60 s per 100 MB of input, capped at 1,800 s — a 1.2 GB firmware child gets about 17 minutes, anything past 2.5 GB the ceilinganalysisthe engine worker is killed and replaced

The 120 s stall threshold sits deliberately between the 20 s tick and the 300 s kill, so an operator gets roughly three minutes of warning before a wedged unpack fails itself.

API routes

93 route paths sit under /api/v1, plus separate operator and compute-farm namespaces that are gated by their own tokens and invisible without them.

RouteWhat is worth knowing
POST /api/v1/uploadMultipart; returns a job id, or a kind: "package" marker when the blob routed to the unpack tier
GET /api/v1/binary/{sha256}Detail; 404 rather than 403 when invisible
GET /api/v1/searchFull-text, facets, and filter= for the DSL; two separate caps — full-text q at 500 characters, DSL filter= at 4,096
GET /api/v1/search/signaturesResolved call sites; max 500 candidate binaries, default 50 results, path prefix ≤512 ASCII bytes
GET /api/v1/similar/{sha256}Jaccard by default; mode=vector and mode=combined are explicit opt-ins
GET /api/v1/compare/{a}/{b}Pro-gated with a 403 upsell before any visibility work; once past the gate, both SHAs must be visible or the answer is 404 for the pair
POST /api/v1/binary/{sha256}/reanalysePro-gated with a 404 cloak; refuses a read-only API key; three outcomes — already_current, in_progress, 202 pending
POST /api/v1/verifyUp to 1,000 hashes per batch; each scoped individually to the caller
GET /api/v1/binary/{sha256}/sarifSARIF 2.1.0; also on the package and source routes
GET /api/v1/binary/{sha256}/stringsPro-gated with a 403 and a typed code, after the visibility 404; offset, VA, encoding, and the functions that cross-reference each string
GET /api/v1/job/{id}/eventsSSE; stage label plus percentage
GET /api/v1/statsFacet counts, scoped to what the caller can see — anonymous gets the public slice, not a corpus total
GET /api/v1/stats/globalThe unscoped twin: whole-corpus facet distributions, the arch × platform coverage matrix, and the package inventory, ignoring visibility entirely. Grant-only, so it 404s for everyone without one and its existence never leaks

Index schema pins

Two index planes carry a version pin, and neither migrates itself — both force an explicit reindex.

PlanePinOn a mismatch
Full-text schemaversion 12a bump wipes the index and the operator runs the reindex; a renamed field's old ordinal still carries the old name, so serving through the change would return wrong terms rather than no terms
Similarityrevision 1, independent of the abovea deployed index at an older revision reports itself unusable rather than answering

The similarity pin is backed by a compile-time guard, because the failure it prevents is silent. The Jaccard rerank blob is stored positionally against the ordered set list, so inserting, removing, or reordering one set — or repairing a weight pairing — would leave every already-stored row comparing set i against a different set i: garbage scores, no error, no log line. A fingerprint over the ordered field names and the weight pairs is asserted at compile time against a recorded constant, so any such change breaks the build until the revision is bumped and deployed indices rebuild.

Measured cost

The catalog holds its shape from 100,000 to 1,000,000 binaries. Measured on a single machine, synthesizing records directly so engine time is excluded. The storage-engine choice these numbers came out of is in Storage architecture.

Operation100K1MShape
Point-get (catalog)17 µs p50126 µs p50Indexed; page-cache-bound
Concurrent reads, peak284K/s at 32 readers231K/s at 128 readersAt 100K, 32 readers is the peak and 128 falls back to 266K/s. At 1M it is a plateau rather than a peak — 228K/s at 32, 231K/s at 128 — while p99 climbs 254 µs → 795 µs: past 32 readers the cost lands on latency, not throughput
Ingest, one row at a time6.7K ops/s5.9K ops/sCorpus-independent
Full catalog scan288K rows/s177K rows/sO(corpus): 0.35 s → 5.6 s
Filtered ANN5.2 ms p5044 ms p50Measured a defect, not the index — see below
End-to-end /similar17 ms p5080 ms p50 / 100 ms p99Same defect underneath; not re-measured since
Boot: open the store228 ms1,434 msNot corpus-independent
Resident memory after seed180 MB304 MBCatalog stays off-heap
Resident memory after the benchmark4,605 MB4,883 MBThe vector and full-text indexes are not off-heap. This, not the 300 MB seed figure, is the serving process's working set

Two planes scale against their own inputs rather than corpus size:

PlaneMeasurementCost tracks
Full-text search, with facets and a visibility filter3.1 ms p50 over 50,000 indexed documentsthe index, not the corpus
SBOM-to-CVE matching, 12-component bill of materialsthe same 240 findings in 8.2 ms against a 10,000-CVE database and 8.2 ms against a 100,000-CVE onethe component fan-out, not the database size

The index-metric mismatch behind the similarity numbers

An IVF_PQ index should scale sublinearly; those two rows scaled near-linearly because the index was never being used. Lance falls back to a brute-force flat scan when a query's distance metric differs from the metric the index was trained with, and it logs rather than errors. The index was built with the default L2 metric while every query asked for cosine, so it was built, kept current, and bypassed on every call — the 13.6 ms of measured flat-scan time is most of the 44 ms wall at 1M.

Three changes followed, and the first is the one that mattered:

ChangeEffect
Train the index with cosine, matching what every query asks forthe index is actually consulted
Fix the probe count at 20 partitions per querypreviously inert — a probe count is meaningless while the query flat-scans
Scalar range indexes on the visibility and file-format columnsthe composed prefilter stays an indexed predicate rather than a scan over the candidate set

Measured at 500,000 rows after the fix: the unfiltered vector query drops from about 14 ms to about 1 ms, and the visibility-and-format-prefiltered query lands at about 6 ms. Raising the probe count from 1 to 50 moves p50 by under a millisecond, which is why 20 is affordable — the metric, not the partition count, was the whole regression.

The full 100K-and-1M benchmark has not been re-run against the fixed index, so the two rows above remain the last end-to-end numbers on record; they are simply not measurements of an IVF_PQ index. The 20-million-binary extrapolation those linear numbers implied — roughly a second per /similar call, seven to fourteen seconds for a 2-hop cluster expansion — does not follow from a sublinear index and should not be carried forward.

Known hot spots

Three smaller costs are dominated by something other than the data the caller asked for — the whole corpus, the whole upload ledger, and parse time rather than disk time.

Hot spotMeasuredShape
Anonymous landing-page aggregate5.6 s at 1M rowsAn O(corpus) walk served from a 30-second cache, so every uncached miss pays it in full
Per-hash sightings rollup14.6 ms at 100,000 ledger rows, 96.7 ms at 650,000Scans the entire append-only upload ledger, not the rows for that hash — grows with total uploads rather than with the binary's own history
Detail-page card-bundle parsing55% of a 12 KB bundle over 5,000 samples; 89% of a 4 MB firmware bundle from a single sampleParsing dominates, not disk reads — which is why a byte-budgeted in-memory cache sits in front of it rather than a count-capped one

Scope and non-goals