StackrootServer-side web stacks and the Linux systems that hold them, from config to debug.

MySQL and SQLite compared

MySQL and SQLite are two relational database engines that solve the same job of storing and querying structured data in two different ways: MySQL is a client-server database that runs as a network service, while SQLite is an embedded engine that lives inside the application process and reads its data from a single file.

A tiny single file sitting on a desk next to an open laptop with a terminal.

The split in the 2026 landscape is concrete. MySQL 8.4 (LTS) and MariaDB 11.6 both run as daemons that accept connections over TCP port 3306 from many clients at once, with InnoDB as the default storage engine and the XA protocol available for cross-engine transactions. SQLite 3.49.1 needs no server, no network stack and no install step on a Linux distro; it ships inside Python, Android and Firefox, and its journal mode gives it atomic commits and a busy timeout of 5000 milliseconds before it raises a SQL_BUSY error. The short version: choose MySQL when many processes or machines need one shared, always-on dataset, and choose SQLite when a single process needs a fast, portable, zero-admin store.

The rest of this page walks the difference the way a practitioner meets it: first the concurrency model, because it decides almost everything, then the deployment shape, then the SQL feature coverage, then the numbers, and finally the decision itself. You will find a full treatment of MySQL in practice as a running service, and a companion MySQL Workbench walkthrough for the GUI side. A sibling page on Choosing between MySQL and PostgreSQL covers the other server-side choice, and the OS layer on various Linux distros shapes the rest of the decision.

How MySQL and SQLite handle concurrent access

Concurrent access is the question that decides the split. MySQL runs as a server process and accepts client connections over TCP; InnoDB gives each row its own lock, so a read and a write on different rows proceed at the same time, and the default isolation level, REPEATABLE READ, is served through a MVCC snapshot rather than a table-wide lock. SQLite, by contrast, takes a single write lock across the whole file. One connection may hold the database for writing while the rest read, but two writers queue behind it and the later one gets SQLITE_BUSY after the busy timeout expires, at which point the application must retry. That is the real trade-off: MySQL buys parallel writers at the cost of a server, and SQLite buys a single file at the cost of a single writer.

Deployment and operations

Deployment is where the two shapes differ most. On a Linux distro, MySQL runs as a systemd unit, listens on port 3306, and keeps its tables under a data directory such as /var/lib/mysql. A database is a set of files the server opens for you, and a backup is a snapshot the server writes with mysqldump or, on InnoDB, with an online copy of the data directory. SQLite is the reverse: the database is the file, for instance /var/lib/app/data.db, and the application opens it directly. There is no socket to manage, no service to restart and no server to upgrade separately from the app. The operational weight moves from the server to the filesystem: back up the file, and the copy is a valid database on its own, with no export step. A practical note: a SQLite file on a network share is a footgun, because NFS does not deliver the file locking the engine depends on. Keep the file local, and the operations are almost trivial.

The server is the product, or the file is the product

The difference shows up in the upgrade path too. Moving MySQL 8.4 to a newer release is a coordinated act: stop the server, upgrade the binaries, and let the server upgrade its own data files in place, which is why a backup before the step is a hard rule. Moving an application that uses SQLite requires no database upgrade at all, because the engine is part of the application bundle and the file format is versioned by the same release. The unit you maintain is the app, not a separate service.

SQL feature coverage

SQL feature coverage is where MySQL has the lead, and the gap is measurable. SQLite implements the core of the SQL standard: transactions, foreign keys, views, triggers, and, from version 3.25.0, window functions. What it does not implement, or implements partially, is the surface a busy server leans on: no XA across engines, no stored procedures in the classic sense, no user-defined aggregate functions in older builds, and a smaller set of character-set and collation options than InnoDB offers. MySQL 8.4 adds window functions, CTEs and JSON types on top of InnoDB, and MariaDB 11.6 layers window functions and generated columns on the same foundation. If your schema needs stored procedures, fine-grained replication, or a rich set of collations, that is a server-side feature, and it pushes the decision toward MySQL. If your schema is plain tables, views and a handful of triggers, SQLite covers it.

Performance and the numbers

Performance follows the model. On a single writer doing local reads and writes, SQLite is often faster than a network round-trip to MySQL, because there is no socket and no protocol overhead, and the engine can keep the whole hot set in memory. The public benchmark that is usually cited puts SQLite in the low single-digit milliseconds per transaction on local storage, while a MySQL write over a loopback connection adds the cost of the client-server hop. The crossover is the writer count: past a single sustained writer, SQLite's single write lock becomes the ceiling, and MySQL's row-level locking keeps throughput climbing as you add writers. For the read-heavy, single-process case, the embedded engine wins; for the many-writers, shared-data case, the server wins.

Where SQLite is the right call

SQLite is the right call in a small, predictable set of situations. It fits an application that runs on one machine and has one writer, such as a desktop tool, a mobile app, or a single-node service that keeps its state in a local file. It fits the test environment, because a fresh database is a fresh file, and you can build a fixture, run the suite and throw the file away with no teardown. It fits the edge, where there is no room or no policy for a server process, and the file can be shipped, synced and queried in place. Android's built-in SQLite is the canonical example: billions of devices run the embedded engine with no server and no install step, and that is the scale at which the zero-admin shape pays off. The rule of thumb is honest: if you can name a second writer, or a second machine that needs the data, you want the server.

Choosing between MySQL and SQLite

Choosing between MySQL and SQLite comes down to three questions. First, who writes: one process or many, and if many, on one machine or across several. Second, what the schema needs: plain SQL, or server-side features like stored procedures, XA and rich collations. Third, how the data moves: a local file you own, or a shared dataset that other systems read and write. The table below lines the two up on the dimensions that actually decide the call.

DimensionMySQL 8.4SQLite 3.49.1
ArchitectureClient-server, network serviceEmbedded, in-process
Default engineInnoDB, XA availableB-tree file, WAL optional
Concurrent writersMany, row-level lockingOne, whole-file write lock
TransportTCP on port 3306None, local file
FootprintServer plus data directoryA single file
Best fitShared, multi-writer, always-on dataSingle-process, portable, test and edge

The decision is a shape, not a score. A single-node service with one writer and a local file is a cleaner SQLite job, and the operations bill is near zero. A web application where several app servers read and write the same rows is a MySQL job, and the server is the point. When in doubt, the test is simple: write down the writer count and the schema, and the shape the two questions point at is your answer.

Where to go next