c3 Limitations
Write concurrency
Single-writer model. SQLite allows only one writer at a time per database. Concurrent write requests to the same Cell are serialized — one waits while the other holds the write lock. Read requests are unaffected and proceed concurrently.
In practice, the queue rarely matters: write operations in SQLite are fast (microseconds to low milliseconds), so the wait is negligible unless you have extreme write throughput. If your workload is write-heavy, minimize the work done per transaction and use batch() to coalesce multiple writes into a single lock acquisition.
No full-text search
SQLite's FTS5 extension is not enabled. Use LIKE for simple substring matching. For heavier search requirements, maintain a separate indexed column with preprocessed text, or route search queries through an external service.
No triggers or stored procedures
CREATE TRIGGER and stored procedures are not supported. Move trigger logic to the application layer — run the equivalent SQL statements in your handler or in a batch() call.
No streaming results
All query results are returned as a complete in-memory array. For very large result sets, use cursor-based pagination (LIMIT / OFFSET or keyset pagination on an indexed column) rather than fetching thousands of rows at once.
Transactions are request-scoped
A transaction cannot span multiple HTTP requests. Begin and complete all atomic work within a single fetch or pulse invocation. The batch() method is the correct tool for multi-statement atomicity within a single handler call.
No cross-database queries
Each c3 binding targets a single database. There is no way to JOIN across two databases in a single query. If you need data from two databases in one response, query each binding separately and join the results in JavaScript.
No connection pooling
Each Cell invocation uses a single database connection for the duration of the request. There is no persistent connection pool between requests. Connection setup is fast, but for very latency-sensitive paths it is worth minimizing the number of round-trips by using batch().
Size and retention
No enforced row limit or size limit. Very large databases increase query latency and memory usage during reads. Keep individual databases under a few hundred megabytes for best performance. Very large BLOB values (images, documents) belong in g7 object storage, not in c3.
Databases persist until explicitly dropped. ribo db drop <name> permanently and irreversibly deletes a database. There is no automatic expiry, retention policy, or recycle bin.
No binary/UUID primary key type
SQLite has no native UUID column type. Store UUIDs as TEXT (36-character hyphenated string) or BLOB (16 bytes). crypto.randomUUID() returns a text UUID suitable for direct storage.