ByteLib Documentation 1.1 Help

Advanced Topics

The SQLite API does a lot under the hood without the developer knowing. Depending on the complexity of your queries or how your plugin is written, these things may affect your plugin's performance or functionality, up to and including crashing the plugin.

This document has been provided to outline some common pitfalls developers may run in to and how to avoid them.

Query Caching

The default configuration for the SQLite API enables an in-memory query cache. This cache stores queries and their results and will return the cached result if a query is executed that matches an existing cache key and the cache is within the configured TTL.

The default cache configuration means there is a potential for stale records to be returned when querying data. The default TTL is 30 seconds, so cache records are considered invalid after this time.

By default, if a cached record is retrieved after 10 seconds but before the TTL, the record is refreshed. Again, by default, the stale value will be served while the data is refreshed.

If a developer wishes to change these default values, they may do so by providing their own values to the CacheConfig parameter of SqliteConfig. See Configuring SQLite API for more information.

Cache Invalidation on Write

Because query cache is keyed on whole SQL queries and not rows or predicates, any operation that writes to the database can change any subset of rows in a table, and the cache has no dependency graph that says:

  • which cached queries read which row

  • which WHERE clauses are affected by a given write

  • whether aggregates like COUNT(*), SUM(...), or joins are now stale

So, after a table or tables is written to, every cached query mentioning that table must be invalidated.

For example, let's say each of the below queries is cached:

  • SELECT * FROM MY_TABLE WHERE id = 5

  • SELECT * FROM MY_TABLE WHERE status = 'ACTIVE'

  • SELECT count(*) FROM MY_TABLE

If a query updates even just one row in MY_TABLE, without full table-level invalidation, any of those caches might now be wrong, and the cache layer cannot prove which ones are still valid.

Query Timeouts

With the SQLite API, queries can time out one of two ways:

  • If the busyTimeoutMs is exceeded while executing a query on a locked table

  • If the mainThreadTimeout is exceeded while running a blocking query on the server's primary thread.

Busy Timeout

The SQLite API sets PRAGMA busy_timeout equal to the value of busyTimeoutMs to prevent timeouts caused by locked tables. This is a SQLite-level feature that will result in a RuntimeException being thrown from the API.

Main Thread Timeout

With the latter, the SQLite API makes most database queries on the main thread. If a query is not run on the Bukkit primary thread (As determined by Bukkit.isPrimaryThread()), then the timeout is null. These queries should never time out.

However, for queries that are executed on the primary Bukkit thread (The default if you are using any method except queryAsync() or executeAsync()), then the query is passed into a CompletableFuture internally, and that future has its timeout set to whatever the mainThreadTimeout is.

If that CompletableFuture times out before completing (i.e., the query did not finish in time), then one of two things will occur based on the API's configured timeout behavior:

  • If timeoutBehavior is set to FAIL_OPEN, then the query result will be null

  • If timeoutBehavior is set to FAIL_CLOSED or THROW, then a DbTimeoutException will be thrown describing what operation caused the timeout, and how long it took to actually time out. The stack trace is also included

Query Threading

Queries are, by default, executed on the main thread. The only time this does not occur is if you use queryAsync() or executeAsync(), or if you execute queries using the AsyncScheduler in Paper.

Depending on the mainThreadPolicy, you may or may not see DbMainThreadDisallowedException or log messages in the server console:

DISALLOW

If mainThreadPolicy is set to DISALLOW, all queries must be executed on separate threads. Attempting to run them on the main thread (As determined by Bukkit.isPrimaryThread()) will result in a DbMainThreadDisallowedException.

WARN

If mainThreadPolicy is set to WARN, queries are allowed to be executed on the main thread. However, if a query takes longer than the configured slowQueryWarnThreshold, then a log message will be output to console that says [ByteLib-DB] Slow main-thread DB [opName]: [elapsed]ms. If you see this message, optimize your queries!

ALLOW

If mainThreadPolicy is set to ALLOW, queries are allowed on the main thread, and no log messages will be output to console, even if the query is blocking or exceedingly slow.

To avoid timeout issues, it is recommended to run queries in separate threads.

11 March 2026