SQLCipher for Android Performance Issue

I’m seeing significant performance issues after migrating from 4.13.0 to 4.14.0 (same issue on 4.14.1). Looking at the logs:

2026-04-23 09:40:39.790 1532-1614 I Database keying operation returned:0
2026-04-23 09:40:40.381 1532-1614 W JNI critical lock held for 582.200ms on Thread[41,tid=1614,Runnable,Thread*=0xb400007acd888840,peer=0x2278468,“DefaultDispatcher-worker-12”]
2026-04-23 09:40:40.566 1532-1603 I Database keying operation returned:0
2026-04-23 09:40:41.137 1532-1603 W JNI critical lock held for 563.490ms on Thread[31,tid=1603,Runnable,Thread*=0xb400007acd871eb0,peer=0x2185560,“DefaultDispatcher-worker-1”]

I notice the JNI critical lock logs used to report values of around 170ms, but now consistently over 500ms. The only thing changed is the sqlcipher dependency version.

Hello @marcardar,

Thank you for the report. Below is some feedback based on your logs:

  1. With the release of SQLCipher 4.14.0 [1] the default cryptographic provider changed to LibTomCrypt for sqlcipher-android Community Edition. There are a host of benefits [2] to the move. With our internal testing we found that the performance impact would be modest for most applications with typical workloads. However, given the timing, it is likely that the change in provider is related to the issue you are reporting. Especially because the keying operation is designed to be time consuming [3] to prevent dictionary and brute-force attacks.
  2. The JNI critical lock held for problems noted in the logs will be resolved in our next release. This adjustment will remove usage of GetStringCritical and GetPrimitiveArrayCritical within the JNI layer, along with their respective release functions. Those critical sections disable the garbage collector from running until released, could also affect timely allocation of new objects, etc. This change may improve performance since it seems that there are a variety of potential performance issues that could occur when executing time consuming operations in critical sections [4][5] (even if the operations become only slightly slower). All the Critical calls will be replaced with non-block API calls instead. This may alleviate some of the issues you are seeing. We will likely have an upcoming release with that change soon.

If that change doesn’t improve the situation, can you share a bit more about the performance issue? For example:

  1. What is the application doing at or around when the keying operation is performed?
  2. How connections are managed in the application?
  3. What devices / hardware are you seeing the issues on?
  4. Does your application utilize WAL mode?

With further review, it may be possible to suggest other changes to address a perceived slowdown.


  1. SQLCipher 4.14.0 Release | Zetetic ↩︎

  2. SQLCipher 4.14.0 Release | Zetetic ↩︎

  3. SQLCipher Design - Security Approach and Features | Zetetic ↩︎

  4. https://bugs.openjdk.org/browse/JDK-8199919 ↩︎

  5. https://bugs.openjdk.org/browse/JDK-6186200 ↩︎

Hi @marcardar,

We recently released SQLCipher 4.15.0 [1] which include the JNI changes mentioned above for sqlcipher-android. When you get the opportunity, would you try the latest release and if you’re still experiencing a performance issue would you mind sharing feedback regarding the questions asked above?


  1. SQLCipher 4.15.0 Release | Zetetic ↩︎

Hello @marcardar,

I just tried 4.16.0. There’s a step in my android app’s first launch where I attach about 5 different databases (I think three are encrypted) and run inserts based on queries on those attached databases. On 4.13.0, this took about 1 second, now takes about 15 seconds. What can I do? GitHub Issue #530

Thank you for sharing some details regarding the timing you are seeing between the two versions, that’s a considerable difference. The questions below will help us better understand how the library is being used in your scenario and will guide our recommendations. Would you please answer the following:

  • How much data is inserted/updated/deleted/queried during this time period?
  • Would you generate a PRAGMA cipher_profile [1] log from both 4.13.0 and 4.16.0 of the offending operations?
  • Are all operations are performed serially on a single connection?
  • What is the key material passed to the attached database:
    • Is it the same key, different keys, raw keys, or regular keys?
  • Does your application utilize WAL mode?
  • Is your application doing anything else at the time these SQL operations are run?
  • How connections are managed in the application?
  • What devices / hardware are you seeing the issues on?
  • Were there any other changes to your application between using 4.13.0 and 4.16.0?

Additionally, would you be able to run the application and collect a SQLCipher trace log for both library scenarios?

PRAGMA cipher_log = '<path to writable file>';
PRAGMA cipher_log_level = TRACE;

  1. SQLCipher API - Full Database Encryption PRAGMAs, Functions, and Settings | Zetetic ↩︎

Thank your for your reply. That’s a lot of questions to go through. Is there something I can try first which might be enough?

To answer some of those questions:

  1. How much data? - On first launch, the app creates a new database (not encrypted) about 50MB in size
  2. I guess a single connection. I open a new database, attach about 5 other databases (about 3 encrypted with different keys) and execute the inserts
  3. I use execute("ATTACH DATABASE '$pathString' AS $handle KEY '$password'")
  4. wal mode is not enabled, and on completion of inserts I execute:

sqldb.execute("PRAGMA synchronous = FULL")
sqldb.execute("analyze")

  1. I don’t think the application is doing anything else during this time. Note - the only change is the sqlcipher version - all other code is the same
  2. I’m not sure if it’s relevant how connections are managed. This is simply a creation of database, attach some other databases, execute inserts and tidy-up (see 4). That’s all done on the same sqlite database/connection
  3. This was tested on a Pixel 7a

Hi @marcardar - thanks for this info. It is helpful because it narrows down a lot of factors, e.g. ruling out connection pooling in WAL mode, multiple threaded connections, other application changes, mid-run application state/load, old hardware, etc.

In terms of minimal further troubleshooting, there are still a few factors here. The slowdown could be contributed to by key derivation, database population (insert into select from across database boundaries is a worst-case for encrypted DBs), or possibly something else.

Probably the single simplest change you could try to narrow this down further would be to use a Raw Key format for the database keys. If you can try this in testing it would eliminate the slowest key derivation step from the mix. Seeing what the performance looks like after that would give us a better idea of what portion of the delta is KDF related.

Is this something you could try in your development environment by swapping out all the regular passwords with raw keys?

I determined that this is nothing to do with keys/passphrase etc (I changed to use raw keys and had the same issue). Instead, it was the query itself which used “UNION” instead of “UNION ALL”.

The regression was introduced when SQLCipher updated its SQLite baseline in version 4.15.0 (so actually unrelated to the original post in this discussion).

  • Before (4.14.1 and below): Based on SQLite 3.51.x. These versions allowed the query planner to use a fast, in-memory hash table for UNION deduplication. For medium-sized dictionary datasets, this was near-instant (~1 second).
  • After (4.15.0 and above): Based on SQLite * 3.53.x. The query planner was updated to consistently use a “sort-and-merge” algorithm for UNION, INTERSECT, and EXCEPT. This change forces a global sort of all results from all attached databases, which ballooned our installation time from 1 second to 13 seconds.

The Fix: We restored the 1-second performance by switching to UNION ALL and implementing a manual two-stage GROUP BY. This effectively “opts out” of the mandatory sorting pass while maintaining 100% data integrity for our bitmasks.

Hi @marcardar that’s great news on the root cause. There have been many changes recently to the upstream SQLite optimizer. This is not the first time we’ve heard about abrupt performance changes for specific queries between versions as a result of those changes. Incidentally this is exactly the sort of thing that PRAGMA cipher_profile is designed to help troubleshoot. By comparing logs between versions it is easy to identify and triage specific statements with a significant performance difference.

At any rate, we’re glad to hear that SQLCipher itself was not the issue, and that you have a fix in place for the application!

I was thinking more generally about this. Is there a possibility of decoupling sqlcipher from the sqlite version? Maybe just enforce a min sqlite version instead. Or failing that, just updating sqlite less often to reduce the number of moving parts with each release?

Hello @marcardar thanks for the additional feedback. To be upfront, decoupling is not really feasible. SQLCipher hooks directly into SQLite internals and uses private APIs. That means news changes to SQLite upstream could break SQLCipher, or new SQLCipher changes might not work with old SQLite versions. We closely monitory and extensively test the SQLCipher / SQLite version combinations accordingly, and only consider the tested combination to be supported.

That said, SQLCipher is already very conservative, compared to other SQLite-derived projects, about upstream adoption. We try hard to only rebase on “stable” SQLite versions (e.g. those where active patching is usually not occurring) and typically run one major version behind the upstream. However, that is not always possible, especially when important security updates are required, but it is the usual case.

As a result, slowing down the adoption of SQLite versions even further isn’t really desirable as a project. Instead, we try to make sure that the right tools are available to identify the cause of bottlenecks. SQLCipher core already includes numerous tools that can be used for this (profile and log settings), in addition to the built-in SQLite functions (explain, etc), and our commercial / enterprise have even more.

Let me know if this makes sense.

Thanks Stephen, that all makes sense.

Considering the tight coupling, have you considered versioning sqlcipher to reflect the sqlite version? For example, 3.53.1.foo. This way, if sqlite did release a security patch for an older version, say, 3.52.1, you would still be able to support that with a 3.52.1.bar release even though 3.53.1.foo is the latest sqlcipher version. Just a thought.

Hi @marcardar, yes, I know that several other projects do versioning based of SQLite’s numbers. However, almost all of those projects are either exceedingly simple, or are basically just language wrappers around the SQLite library. Neither of those apply to SQLCipher’s situation, where there are meaningful functional enhancements, separate compatibility considerations, independent long-term development, and multiple extended packages.

To directly address the subject of backports, in almost 20 years of following SQLite development I can only recall a single time there has been an officially communicated backported release, and even then it was fossil only. The SQLite development and release process is very linear, and SQLCipher’s follows that lead.

I’m definitely not trying to discourage discussion on the topic, and we remain receptive to feedback. We’ve extensively considered many different version strategies over this project’s long history. While no versioning system is perfect, we do feel strongly that versioning separately from SQLite is appropriate. That said we do occasionally make changes to address issues or improve clarity, and we’ll consider the feedback on the thread in the future.