Semantic search: independent spend and abuse audit

Audit date: 2026-10-12. Reviewer: independent automated search/spend review. Human approval: unsigned. Scope: actual account/search/abuse adapters and proposed Workers AI + Vectorize integration; no production deployment, network inference, invoice reconciliation or provider deletion verification is implied. This document supplements security gates, search specification, and the decision ledger in DEVLOG.

Conclusion

A per-account allowance cannot bound a service's total bill. Enabling paid semantic search requires durable global admission before every inference and vector operation, durable owner allowances, and a ceiling on retained vectors/namespaces. Cloudflare billing alerts and free included allocations on a paid account do not supply these controls. Implementation reconciliation below distinguishes new local safeguards from remaining production gates.

The initial snapshot had lexical search in the real account runtime, with semantic provider ports unused and embeddingAllowed:false. No paid semantic work occurred through that runtime. Creating an empty remote index establishes a resource, not a functioning or cost-verified deployment.

Initial findings and required controls

ID Finding in inspected source Severity if paid semantics enabled without changes Required control / disposition
S01 Search HTTP throttles are 10/minute/IP, 50/hour/IP and 20/5 minutes/handle; document indexing, alarms and rebuilds do not pass this route. No owner monthly or global paid-work reservation existed. High Enforce durable owner and aggregate allowances inside provider paths, including retry/rebuild/background work. Fail closed if accounting is absent or corrupt.
S02 SearchService.ingest embedded before the vector adapter rejected a repeated revision. Every fresh identical query embedded again. Provider/acknowledgment failures left work pending. High Check durable model+revision before embedding; charge every attempted external request, preserve charges on ambiguous failure; bounded retry/dead-letter policy. Identical-query caching is optional optimization, never the cost boundary.
S03 Vector upsert reached the provider before local state recorded its vector ID. A crash between these steps could orphan a remote record. Tombstones decremented active usage before remote deletion completed. High Persist intent before remote writes. Track active, pending and cleanup capacity separately; keep outstanding work discoverable, with monotone lifetime admission if reclaim cannot be proven.
S04 Account farming multiplies owner allowances; admission allows up to 200 signups per 24-hour window globally, without a lifetime active-account ceiling. High Independent global operation and stored-capacity ceilings; explicit semantic enrollment/namespace admission. Turnstile and IP/subnet limits remain defense in depth.
S05 One owner namespace per index; no index routing/admission scheme existed. Medium Cap enrolled owners and vector slots below provider limits, then prove bounded shard routing/migration before expansion.
S06 Document maxTokens options passed by the service were ignored by the original Workers AI adapter, which accepted up to 33,300 characters. Medium Pin model/dimension and a conservative bounded excerpt; account pessimistic model input units per call. Passing a token hint is not enforcement.
S07 Blob quota, lexical token count and active document/vector count do not bound all SQL metadata, retained tombstones, retry records, account resources or provider storage. High at scale Actual per-account SQL byte budget, bounded metadata counts/retention and measured garbage collection. Preserve separate release gate until implemented and measured.

The billing plan must come from verified entitlement, not a requested tier. Paid storage does not implicitly authorize an unlimited semantic allowance. Missing or stale billing should never relax spending limits.

Account farming, network identity and contact bearers

Signup attempts are separately limited to five/minute/IP and 100/hour/subnet. The reviewed stricter admission policy requires one signup per three hours/IP, two/day/IP, 15/day/subnet, five/hour/country, 25/day/country, ten/hour globally and 200/day globally. Region is the two-letter country from trusted request.cf.country; a caller-supplied country header is ignored, and Node uses the shared unknown region. These limits run after challenge/terms/method/handle checks and before expensive account creation. Sequential windows conservatively retain earlier debits when a later limit rejects. Local test configuration deliberately relaxes the IP cooldown; it is not the production setting. These are anchored fixed windows, not precise rolling windows; work can cluster across boundaries. Distributed addresses evade local limits, while the global counter still bounds admission rate. The new lifetime admission counter additionally stops after 1,000 new signup reservations; it never resets with daily windows. This counts new admissions after the counter is introduced, including conservatively charged failed creation, not existing accounts or proven active users. Inventory existing accounts separately. A thousand additional accounts represent up to 500 GB of free payload allowance, before physical copies/index/metadata; maintaining them remains a recurring liability. Without the lifetime gate, the daily rate alone would permit 73,000 admissions over 365 days (36.5 TB of logical allowance). These are allowance scenarios, not observed usage.

Node's actual HTTP adapter strips CF-Connecting-IP, X-Forwarded-For and X-Real-IP, then uses the socket peer. Thus direct client spoofing does not bypass its limiter. Behind a reverse proxy, the socket identity collapses clients into the proxy unless an explicitly trusted adapter is added. Direct calls to the test fetch helper are not evidence about HTTP trust. Worker entry relies on Cloudflare's supplied client-IP header; production ingress, same-zone Worker subrequests and any alternate entry path need a deployment-specific trust test. HMAC pseudonyms reduce retention of raw addresses but neither prove a unique human nor stop address rotation. IPv4 /24 and IPv6 /48 normalization are coarse abuse groupings, with shared-network false positives; they are not an entitlement boundary.

Public contact deliveries require cryptographic bearer verification and intersect recorded grant constraints. Contact message/byte allowances and account blob reservations protect those resources. HTTP contact/message throttles are sender-IP based; one token or many valid tokens from distributed senders can still consume a recipient's resources. A sender must never authorize remote embedding of another account's content. Semantic opt-in and limits must be owner-controlled, and message-triggered background embedding must spend the same owner/global budgets as explicit requests. When indexing reaches a semantic limit, the agreed behavior should preserve lexical indexing and readable authoritative content. Search queue backpressure and message rejection remain separate product policy.

Provider facts checked against official documentation

Prices and limits below were read on the audit date; they must be revalidated when enabling or changing the provider.

Resource Published terms relevant to this implementation
BGE small @cf/baai/bge-small-en-v1.5, 384 output dimensions, maximum 512 input tokens. Model page quotes $0.0202/million input tokens. Model documentation
Workers AI billing 10,000 neurons/day included; Workers Paid overage is $0.011/1,000 neurons. Pricing table gives BGE small 1,841 neurons/million tokens and a rounded $0.020/million tokens. Multiplying neuron pricing gives $0.020251/million. Reserving 11 micro-USD per 512-token attempt is conservative at these rates. Free-plan exhaustion stops usage; a paid-plan included allowance is not a spending ceiling. Pricing
AI throughput Text embedding default 3,000 requests/minute. Wrangler local-mode inference can consume remote usage. Provider rate limits constrain throughput, not aggregate monthly expense. AI limits
Vectorize billing Paid includes 50 million queried dimensions/month and 10 million stored dimensions; overage $0.01/million queried dimensions and $0.05/100 million stored dimensions. Free includes 30 million queried and 5 million stored dimensions. Published queried-dimension examples use (queries + stored vectors) × dimensions, not queries × all stored vectors × dimensions. CLI/API queries count too. An empty index incurs no stored dimensions. Vectorize pricing
Vectorize capacity V2 limit: 20 million vectors/index; 50,000 namespaces/index paid, 1,000 free; namespace and vector ID up to 64 bytes; at most 50 results with metadata. Older examples still mention lower namespace counts; use the current limits page and actual account limits. Vectorize limits
SQL account size Paid SQLite Durable Objects have 10 GB/object; unlimited paid account aggregate storage is not a free allowance. An individual object has a soft 1,000 requests/second limit and remains single threaded. Durable Objects limits
Billing notifications Cloudflare says budget alerts only notify and do not stop usage; usage processing is daily. They are an operational signal, not a pre-request circuit breaker. Official budget-alert announcement

Illustrative gross price calculations exclude included allocations, SQL/Worker/R2 costs, fees and operational work. A 1,000-document + 500-query monthly owner allowance at 512 charged tokens/attempt uses at most 768,000 budget tokens. At the conservative 11 micro-USD/attempt, inference admission reserves $0.0165. At 384 dimensions, 1,000 stored vectors and 500 queries give approximately $0.005952/month using Vectorize's published formula. These are estimates under fixed assumptions, not a guaranteed Cloudflare invoice or an economically complete product margin.

At the original lexical paid limit, 100,000 documents × 32 KiB is 3.2768 GB of body text before titles. The implementation stores content in both search_documents and the contentful FTS table: approximately 6.5536 GB of duplicated body text alone. Postings, catalogs, tags, JSON records and tombstones are additional. The 20-million lexical-token ceiling does not prevent this case: punctuation or a very long unbroken word consumes bytes with few lexical tokens. A 100 GB blob entitlement therefore cannot establish safe SQL capacity. Deletes retain revision markers to reject replay, so churn needs its own retention/compaction proof.

Required production checks

  1. Demonstrate atomic concurrent admission at owner and global ceilings. Once a limit is reached, count actual mocked provider invocations and prove none start. Repeat after object/process restart, period rollover, provider timeout and malformed accounting/configuration.
  2. Charge operations before sending; never refund ambiguous timeouts. Every additional remote attempt needs a fresh durable charge. Count query embeddings, document embeddings, Vectorize reads/writes and relevant cleanup/migration work. Keep cleanup possible when ordinary ingestion is disabled, within a separate bounded operations allowance.
  3. Keep stored-capacity reservations across billing periods. Namespace/slot ceilings must not reset each month. Reclaim only after provider deletion completion and local reconciliation; otherwise use conservative unreclaimed capacity. A current-active-vector counter does not count leaked or pending vectors.
  4. Bound retries/backoff and give poison records a durable exhausted/deferred state. A finite monthly inference cap limits paid inference but does not cap limitless local alarm/read costs. Prove lexical indexing progresses around semantically deferred items.
  5. Verify returned vectors, namespace isolation, live ACL/revision checks, consent revocation during in-flight work and cached cursor use. Demonstrate provider cleanup after revocation and deletion; queued deletion acknowledgment is not erasure evidence.
  6. Measure source extraction, SQL bytes/rows, vector churn and request costs with realistic small and large accounts. The implemented budget path uses sixteen fixed partitions and prepaid owner leases; load-test partition contention, owner SQL debit overhead and lease turnover before scale. Shards must share a finite allowance, never each inherit the entire global ceiling.
  7. Reconcile provider usage to reservation estimates and document model/pricing drift response. Turn off new paid work on unexplained divergence. Record provider acceptance, persistence and eventual deletion separately. Add alerts as backup, not substitutes for admission.
  8. Preserve production release/semantic opt-in gates until privacy, relevance, lifecycle deletion and operational approval are signed. A configured key/binding/index or passing injected tests does not resolve them.

Implementation reconciliation and evidence

The initial high-risk gaps now have application-level controls in the local implementation:

Independent node --test tests/semantic-spend-audit.test.mjs passed 13/13 after the accounting and lease fixes. It tests concurrent global ceiling admission, controller reconstruction over the same SQLite state, permanent stock ceilings across monthly rollover, corrupted owner/global accounting, nonrefunded provider timeouts, global denial before inference, UTF-8 excerpt boundaries, ambiguous-upsert cleanup intent with an unreclaimed lifetime slot, exact aggregate conservation across sixteen uneven partitions, one selected vector binding without fan-out, durable lease reuse, midnight response rejection, and concurrent stock-only grant merging without restoring spent money. The last test demonstrated and caused correction of a stale lease overwrite race. This is injected local evidence; it does not show Cloudflare billing, latency, deletion completion or reboot/disk failure behavior. The coordinator owns the combined npm test, actual Worker and browser verification record in DEVLOG.

Remaining production gates: global dollar figures cover admitted inference/vector operation estimates, not ongoing vector storage or all Cloudflare charges. At two million lifetime 384-dimensional remote records, gross stored-dimension cost alone is approximately $0.384/month at the current published rate, even when new paid work stops. If that corpus is queried, the stored-vector component in the published queried-dimensions formula adds another $7.68/month. Thus $5 of admitted operation estimates can coexist with approximately $13.064/month of direct semantic exposure before shared allowances and other infrastructure; at one million vectors the corresponding comparison is $9.032. Owner ceilings may make actual reachable storage lower. Source SQL/FTS, retained tombstones, abuse guard objects, ingress, R2 and alarm costs remain outside these estimates. Cleanup calls bypass ordinary consent/spend admission so revocation can proceed. The later work spend audit adds persisted backoff and an eight-failure automatic pause; operator recovery and operational cost proof remain gates. Tombstone failures now use the semantic failure callback too, allowing lexical queue progress while durable vector cleanup receipts remain; provider deletion completion remains unproven. No hard cap on the entire provider invoice is claimed. Full account erasure, measured SQL metadata ceilings, funded retained-storage liability and production provider reconciliation remain unresolved release work.

Independent economics cross-review reproduced the calculator's account tables, fleet totals, taxes/card assumptions and 23-Plus/4-Pro small-fleet break-even result under its stated assumptions. It identified a memory-unit conversion discrepancy: Cloudflare's own duration example uses 128 MB as 0.128 GB, while the first calculator used 0.125. The economics author corrected the model and regenerated derived totals. Large unrestricted semantic workloads in that model are sensitivity cases, not the smaller launch runtime allowance. Official duration example

Final sharding and lease review

Semantic layout version 1 selects one of four Vectorize bindings by the first SHA-256 digest word of the owner ID modulo four. The account persists its layout/shard and refuses a conflicting route. Each query, write and delete uses that one binding, with the owner namespace applied inside it; ordinary requests do not query every index or consult a global routing directory. The adapter retains an explicit version-zero single-binding path for injection/legacy configuration; changing an account's stored route requires migration. Creating the four indices and configuring bindings is provisioning evidence only. It does not prove a deployed application or migrated corpus.

Budget routing uses the corresponding stable hash modulo sixteen and the fixed names semantic-spend-v1-<partition>. Division plus remainder distributes every configured aggregate limit exactly. For example, 1,000 owner slots become eight partitions of 63 and eight of 62. A budget object verifies the owner maps to its partition and persists an immutable layout/partition marker. The caller's durable lease layout also records version/count. This removes the single per-operation global budget object: each account spends prepaid local balances until it needs another grant.

The final lease size is 128 micro-USD for money and 16 vector upsert slots, requested independently. Query-only claims do not reserve vector stock. Authority admission durably charges a grant before returning it. A local transaction merges that grant into the latest persisted balance, then another durable transaction debits the actual operation before the provider starts. Grant responses must match owner, partition, layout version, UTC day/month and requested amounts. Controller reconstruction reuses persisted balances. A crash after authority debit but before local persistence loses capacity rather than creating extra allowance; ambiguous outcomes receive no refund. A response arriving after its quoted UTC day is rejected. Counters bound admission within UTC periods, not the provider's eventual invoice date for in-flight work.

Unused money and stock leases expire locally at UTC day change; their authority reservations remain consumed, including permanent stock slots. This is conservative and can reduce usable capacity. With $0.50/day divided across sixteen partitions, fixed 128-micro-USD grants can issue at most 3,904 full money grants/day; the leftover 18 micro-USD per partition cannot fund another full grant. One isolated query/day reserves 128 micro-USD while spending 15 locally, so $5/month can admit only about 39,056 full grants, not $5 divided by the query price. Uneven owner/traffic distribution may exhaust one partition while another has spare allowance. There is no automatic borrowing or redistribution and no refund on restart/expiry. These are availability/fairness limitations, not additional spending permission.

Global stock includes leased-but-unused slots; actual stored vectors and successful upserts may be lower. Owner lifetime admission still separately restricts attempts. A migration must preserve both spent and outstanding reservations and avoid two active layouts each allocating the full ceiling. Fixed partition count, different bindings or a fresh namespace must never be treated as a free reset of existing obligations. No automatic migration from the prior singleton budget or identity layout is claimed.

The cost of local per-operation accounting remains: owner budget writes, lease debits, metadata lookups, and occasional remote grant transactions. Leases amortize remote budget RPCs; they do not eliminate accounting SQL/DO costs or demonstrate horizontal throughput. Measure this overhead and retry churn before using the economics model as a launch budget.

Independent identity cross-review examined identity-shards.mjs, its recovery protocol and thirteen passing tests. The versioned 64-bucket-per-family scheme preserves token-checked uniqueness claims and authoritative CAS, with bounded interrupted-operation recovery and conditional cleanup. Ordinary account session reads/CAS remain on the account bucket. No demonstrated critical uniqueness/CAS defect remained in this review. Explicit fresh-layout/migration gating, fixed partition capacity, unscheduled full maintenance, expired-device/failed-operation retention and actual provider load remain operational gates. The combined independent run of semantic spending and identity sharding passed 26/26. This is automated evidence, not human release sign-off.