{"title":"SQLite's synchronous = NORMAL, And When Not To Use It","html_url":"https://deallocatedobjects.com/posts/sqlite-synchronous-normal-and-when-not-to-use-it","source_url":"","markdown_content":"[Postage](https://postageformac.com/?ref=deallocated-post), my Mac email client, keeps two SQLite databases. `emails.db` is a cache of mail that can always be downloaded again, and `rules.db` holds data that exists nowhere else. Both run in WAL mode, and only the cache uses `synchronous = NORMAL`:\n\n```swift title=\"DatabaseManager.swift\"\ndb = Self.open(directory.appendingPathComponent(\"emails.db\").path)\n// Everything in here can be re-downloaded.\nSelf.exec(db, \"PRAGMA synchronous = NORMAL;\")\nrulesDb = Self.open(directory.appendingPathComponent(\"rules.db\").path)\n```\n\n`open` sets `journal_mode = WAL` on both. `journal_mode` is stored in the database file, but `synchronous` belongs to the connection, so it has to be set every time you open one.\n\nAt the default `FULL`, each commit syncs the WAL to disk. At `NORMAL`, commits skip that sync and only checkpoints do it. For 500 single-row commits on my Mac, that took the cache from 0.49 seconds to 0.23 seconds. Postage commits for every loaded message body and flag change, so those add up. Work you already batch into one transaction gains much less, because it only has one commit to begin with.\n\n### What You Give Up\n\nThe [SQLite docs](https://www.sqlite.org/wal.html) are direct about the trade. In WAL mode, `NORMAL` can't corrupt the database, but it does lose durability: a transaction that committed just before a power loss or hard reboot can roll back. If only your app crashes, the data is safe, because the operating system still has the writes.\n\nFor the mail cache, that means the newest few writes might disappear, and the next refresh fetches them again.\n\n### When To Keep FULL\n\nI ask whether the data could come back from somewhere else after a power cut. The mail is still on the IMAP server, so the cache gets `NORMAL`. Rules and their log exist only in that file, so `rules.db` keeps `FULL`.\n\nThe same goes for any write you've confirmed to someone else, like a payment record, where losing it means two systems disagree and nobody finds out.\n\nCheck your journal mode before you copy this line. Outside WAL, `NORMAL` carries a small risk of corruption from a badly timed power loss on older filesystems, so this reasoning only holds with `journal_mode = WAL`.","html_content":"<p><a href=\"https://postageformac.com/?ref=deallocated-post\">Postage</a>, my Mac email client, keeps two SQLite databases. <code>emails.db</code> is a cache of mail that can always be downloaded again, and <code>rules.db</code> holds data that exists nowhere else. Both run in WAL mode, and only the cache uses <code>synchronous = NORMAL</code>:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"file\">DatabaseManager.swift</span><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">db </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> Self</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">open</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(directory.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">appendingPathComponent</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"emails.db\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">).path)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6A737D;--shiki-dark:#6A737D\">// Everything in here can be re-downloaded.</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">Self</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">exec</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(db, </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"PRAGMA synchronous = NORMAL;\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">rulesDb </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> Self</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">open</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(directory.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">appendingPathComponent</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"rules.db\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">).path)</span></span></code></pre></figure><p><code>open</code> sets <code>journal_mode = WAL</code> on both. <code>journal_mode</code> is stored in the database file, but <code>synchronous</code> belongs to the connection, so it has to be set every time you open one.</p>\n<p>At the default <code>FULL</code>, each commit syncs the WAL to disk. At <code>NORMAL</code>, commits skip that sync and only checkpoints do it. For 500 single-row commits on my Mac, that took the cache from 0.49 seconds to 0.23 seconds. Postage commits for every loaded message body and flag change, so those add up. Work you already batch into one transaction gains much less, because it only has one commit to begin with.</p>\n<h2 id=\"what-you-give-up\" data-level=\"3\">What You Give Up</h2>\n<p>The <a href=\"https://www.sqlite.org/wal.html\">SQLite docs</a> are direct about the trade. In WAL mode, <code>NORMAL</code> can’t corrupt the database, but it does lose durability: a transaction that committed just before a power loss or hard reboot can roll back. If only your app crashes, the data is safe, because the operating system still has the writes.</p>\n<p>For the mail cache, that means the newest few writes might disappear, and the next refresh fetches them again.</p>\n<h2 id=\"when-to-keep-full\" data-level=\"3\">When To Keep FULL</h2>\n<p>I ask whether the data could come back from somewhere else after a power cut. The mail is still on the IMAP server, so the cache gets <code>NORMAL</code>. Rules and their log exist only in that file, so <code>rules.db</code> keeps <code>FULL</code>.</p>\n<p>The same goes for any write you’ve confirmed to someone else, like a payment record, where losing it means two systems disagree and nobody finds out.</p>\n<p>Check your journal mode before you copy this line. Outside WAL, <code>NORMAL</code> carries a small risk of corruption from a badly timed power loss on older filesystems, so this reasoning only holds with <code>journal_mode = WAL</code>.</p>","created_at":"2026-11-10T14:00:00.000Z","updated_at":"2026-11-10T14:00:00.000Z"}