• Home
  • About
  • Portfolio
  • Contact
CodeCurious
  • Home
  • About
  • Portfolio
  • Contact
Go Back

Optimizing SQLite For Rails 8 Production: A Complete Guide

Learn how to optimize SQLite for Rails 8 production with WAL mode, busy timeouts, Solid Queue, and database.yml best practices.

Jean Emmanuel Cadet
By Jean Emmanuel Cadet • Ruby on Rails Developer

Last updated : Jun 20, 2026 • 14 min read

Optimizing SQLite for Rails 8 Production: A Complete Guide

Last updated : Jun 20, 2026 • 14 min read

Share with friends

Rails 8 made an important change: SQLite is no longer just for development. For years, developers reached for PostgreSQL or MySQL the moment an app went live. That was the safe, conventional choice. But conventions are shifting.

Rails 8 ships with SQLite support baked into its defaults, making it genuinely suitable for many production workloads. With enhancements like Solid Queue, Solid Cache, and Solid Cable, you can run a full-featured Rails application on a single server using only SQLite as your data engine. No Redis. No external database server. No extra infrastructure to maintain.

This guide is for Rails developers who want to understand how to configure SQLite correctly for production. You will learn about WAL mode, busy timeouts, PRAGMA settings, Solid Queue setup, and the practical limits of this approach so you can make a confident, informed decision.


Why Use SQLite in Production?

SQLite has a reputation as a toy database. That reputation is no longer fair for a large class of applications.

Simplicity

There is no database server to install, configure, or secure. Your database is a file on disk. Deployments become simpler, and there are fewer moving parts that can break.

Lower Infrastructure Costs

Running PostgreSQL on a managed cloud service costs money. With SQLite, you eliminate that dependency. For solo developers, small teams, or early-stage products, this savings adds up quickly.

Easier Deployment

When you deploy with Kamal (Rails 8's default deployment tool), your entire application, including the database, lives on one server. Migrations are straightforward, rollbacks are easier to reason about, and you avoid network latency between your app and database.

Backups and Maintenance

Backing up a SQLite database is as simple as copying a file. Tools like Litestream can stream SQLite changes to an S3-compatible bucket in real time, giving you continuous backups without complex configuration.

Suitable Use Cases

SQLite in production works well for:

  • Internal tools and admin dashboards
  • Personal projects and side products
  • Early-stage SaaS with moderate traffic
  • Content-heavy sites with mostly read operations
  • Applications with a single region deployment

Limitations You Should Understand

SQLite is not a good fit for every application. A few honest limitations:

  • Write operations are serialized. Only one write can happen at a time.
  • You cannot easily scale horizontally across multiple servers sharing the same database.
  • Very high write throughput will eventually hit a ceiling.
  • You cannot use read replicas the same way you would with PostgreSQL.

Keep these in mind as you read the rest of this guide.


Understanding SQLite Concurrency

Before tuning SQLite, it helps to understand how it handles concurrent database access.

How SQLite Handles Reads and Writes

SQLite uses file-level locking. When a write happens, it locks the database file so other writes must wait. Reads, however, can usually proceed concurrently with writes when WAL mode is enabled (more on that in a moment).

In the default journal mode (called DELETE or rollback journal mode), even reads can be blocked while a write transaction is open. This causes problems under any real-world load.

File-Based Database Architecture

Because SQLite stores everything in a single file, it does not have a separate server process managing connections. Every connection goes directly to the file. This is what makes it simple, but it also means Rails connection pooling and SQLite locking behavior must be understood together.

Common Locking Issues

Without proper configuration, you will see errors like:

SQLite3::BusyException: database is locked: database is locked

This happens when one thread or process holds a write lock and another times out while waiting. Out of the box, SQLite has a very short timeout before it gives up and raises this error.

What Happens Under Load

With the default SQLite settings and multiple Rails threads, even light traffic can produce locking errors. The solution involves two key changes: enabling WAL mode and setting a reasonable busy timeout.


Enable WAL Mode

Write-Ahead Logging (WAL) is the most important SQLite optimization you can make for any production application.

What Is WAL Mode?

In WAL mode, SQLite writes changes to a separate log file instead of directly modifying the database file. Readers continue accessing the original database file while writers work on the log. This means reads and writes can happen concurrently without blocking each other.

Benefits of WAL Mode

  • Readers do not block writers, and writers do not block readers
  • Significantly better performance under concurrent access
  • Faster write performance in most workloads
  • Crash recovery is safer and faster

How to Enable WAL Mode in Rails

You can enable WAL mode in a Rails initializer or via a migration. The recommended approach is to set it as a connection option that runs automatically.

Create a file at config/initializers/sqlite.rb:

# config/initializers/sqlite.rb
module SqliteConfig
def configure_connection
super
db = @connection
db.execute("PRAGMA journal_mode=WAL;")
db.execute("PRAGMA synchronous=NORMAL;")
db.execute("PRAGMA cache_size=5000;")
db.execute("PRAGMA foreign_keys=ON;")
end
end

ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SqliteConfig)

Alternatively, Rails 8 supports configuring PRAGMAs through database.yml via the pragmas key (more on that in the configuration section).

Verify WAL Mode Is Enabled

Connect to your SQLite database and run:

sqlite3 db/production.sqlite3 "PRAGMA journal_mode;"

The output should be wal. If it returns delete, WAL mode is not active.


Configure Busy Timeouts

Even with WAL mode enabled, write operations are still serialized. When two writes compete, one must wait. The busy timeout controls how long SQLite waits before giving up and raising an error.

What "Database Is Locked" Errors Mean

A locked database error means a write attempted to acquire a lock, but another write was already holding it. SQLite's default timeout is zero milliseconds, which means it gives up immediately. For any real application, this will cause errors under even light concurrent load.

How Busy Timeouts Work

When you set a busy timeout, SQLite will retry acquiring the lock repeatedly for the specified duration before giving up. For most applications, setting a timeout between 5,000 and 30,000 milliseconds gives writes enough time to queue and succeed without making users wait too long.

Example Rails Configuration

You can set the busy timeout directly in database.yml:

production:
adapter: sqlite3
database: db/production.sqlite3
timeout: 5000

The timeout key maps directly to SQLite's busy timeout in milliseconds.

If you need more control, you can also set it via PRAGMA in your initializer:

db.execute("PRAGMA busy_timeout=5000;")

Trade-offs and Best Practices

A longer busy timeout means requests will queue longer before failing. This can mask deeper problems. A good rule of thumb:

  • Set the timeout to at least 5,000 milliseconds for production
  • Monitor your application for slow requests, which can indicate write contention
  • If you frequently hit the timeout limit, your write volume may be outgrowing SQLite

Configure Rails 8 Database Settings

Here is a complete, production-ready database.yml configuration for a Rails 8 app using SQLite.

# config/database.yml
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000

development:
<<: *default
database: db/development.sqlite3

test:
<<: *default
database: db/test.sqlite3

production:
<<: *default
database: db/production.sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
# Rails 8 pragma support
pragmas:
journal_mode: wal
synchronous: normal
mmap_size: 134217728 # 128MB
journal_size_limit: 67108864 # 64MB
cache_size: 2000
foreign_keys: true

Connection Pool Settings

The pool setting controls how many database connections Rails can hold open at once. Set this equal to your Puma thread count. If RAILS_MAX_THREADS is 5, your pool should be 5.

Using more connections than your pool allows leads to ActiveRecord::ConnectionTimeoutError errors.

Production Recommendations

  • Keep pool aligned with your Puma threads
  • Use timeout to set a busy wait, not the ActiveRecord connection checkout timeout (those are different settings)
  • Store the database file on a fast disk, ideally an NVMe SSD
  • Avoid putting the database file on a network-mounted volume

Using Solid Queue with SQLite

What Is Solid Queue?

Solid Queue is a database-backed background job queue that ships as a Rails 8 default. It uses Active Record to store jobs in the database, which means no Redis, no Sidekiq, and no external queue server.

Why Rails 8 Uses It by Default

Historically, background jobs required Redis and a gem like Sidekiq or Resque. Rails 8 changes this default. For most applications, a database-backed queue is fast enough and dramatically simpler to operate.

Benefits Compared to External Queue Systems

  • No Redis to install, configure, or monitor
  • Job data stays in your database alongside your application data
  • Simpler deployment and fewer failure points
  • Consistent transactions (jobs can be enqueued inside a database transaction)

How to Configure Solid Queue with SQLite

Solid Queue is configured through config/queue.yml. Here is a production-ready setup:

# config/queue.yml
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 3
polling_interval: 0.1
processes: 1

production:
<<: *default
workers:
- queues: "default,mailers"
threads: 3
polling_interval: 0.5
processes: 1
- queues: "low"
threads: 1
polling_interval: 2
processes: 1

Solid Queue uses a separate database by default in Rails 8. Here is how to configure that in database.yml:

production:
primary:
adapter: sqlite3
database: db/production.sqlite3
pragmas:
journal_mode: wal
synchronous: normal
foreign_keys: true
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
queue:
adapter: sqlite3
database: db/production_queue.sqlite3
pragmas:
journal_mode: wal
synchronous: normal
pool: 5
timeout: 5000
migrations_paths: db/queue_migrate

Separating the queue database from the primary database is a best practice. It reduces write contention on the primary database and lets you tune each database independently.

Common Pitfalls to Avoid

  • Do not run too many Solid Queue workers. More threads mean more concurrent writes, which increases contention.
  • Avoid very short polling intervals in production. Polling every 100ms with many workers adds up to significant database load.
  • Monitor the solid_queue_jobs table size. Stale failed jobs accumulate over time.

Additional SQLite Production Optimizations

Key PRAGMA Settings

Beyond WAL mode and busy timeouts, these PRAGMAs improve performance:

PRAGMA synchronous = NORMAL;

By default, SQLite syncs to disk after every transaction (FULL mode). NORMAL mode syncs less aggressively and is safe with WAL enabled. You trade a tiny risk of data loss on a hard crash for noticeably better write performance.

PRAGMA cache_size = 2000;

This sets the page cache size. Each page is typically 4KB, so 2000 pages means about 8MB of cache in memory. Increase this if your server has RAM to spare.

PRAGMA mmap_size = 134217728;

Memory-mapped I/O lets SQLite read database pages directly from memory-mapped files. This can significantly speed up reads on modern operating systems. The value above is 128MB.

PRAGMA foreign_keys = ON;

SQLite does not enforce foreign key constraints by default. Turn this on to catch data integrity issues.

Database Maintenance

Run VACUUM periodically to reclaim space from deleted rows:

sqlite3 db/production.sqlite3 "VACUUM;"

Run ANALYZE to update query planner statistics:

sqlite3 db/production.sqlite3 "ANALYZE;"

You can automate these with a Solid Queue recurring job or a cron task.

Monitoring and Backups

Litestream is the recommended tool for SQLite backups in production. It streams WAL changes to S3, R2, or another object storage service with sub-second latency. Your recovery point objective can be under a second.

A basic Litestream configuration looks like this:

# litestream.yml
dbs:
- path: /app/db/production.sqlite3
replicas:
- type: s3
bucket: your-backup-bucket
path: production/db
region: us-east-1

Common Mistakes to Avoid

Not enabling WAL mode. This is the most common mistake. Without WAL mode, reads block writes and vice versa. Always enable WAL in production.

Using the default busy timeout of 0. With zero timeout, any write contention immediately raises an error. Set it to at least 5,000 milliseconds.

Setting the connection pool higher than your thread count. Extra connections do not help and waste resources. Match the pool to RAILS_MAX_THREADS.

Storing the database on a network file system. NFS, EFS, and similar network volumes can corrupt SQLite databases. Use the local disk only.

Running too many background job workers. With Solid Queue and SQLite, each worker thread is a potential concurrent writer. Start conservative and scale up carefully.

Forgetting to set up backups. SQLite, being a file, does not mean it cannot be lost. Use Litestream or another backup strategy before you go live.

Ignoring slow query logs. Rails 8 includes the Query Logs feature. Enable it and periodically review slow queries to catch missing indexes early.


When SQLite Is the Right Choice

SQLite production deployments work best when:

  • Your application runs on a single server
  • Write operations are moderate (hundreds to low thousands per minute)
  • Your team values operational simplicity over raw scalability
  • You are building an internal tool, SaaS MVP, or personal project
  • You want to reduce infrastructure costs in the early stages of a product

Many successful SaaS applications run at revenue levels that never require more than a single well-tuned server. SQLite can take you further than you might expect.


When to Move to PostgreSQL

There are clear signals that it is time to switch:

  • High write volume. If your write queue is consistently backed up or your busy timeout is regularly triggered, you have likely outgrown SQLite's serialized write model.
  • Multiple app servers. If you need to run more than one web server process and they share a database, SQLite is not the right tool. It cannot safely be accessed over a network by multiple machines.
  • Complex scaling requirements. Read replicas, connection poolers like PgBouncer, and horizontal sharding are PostgreSQL features that SQLite does not support.
  • Enterprise or large team environments. Teams that need fine-grained database permissions, audit logging, or point-in-time recovery at scale will outgrow SQLite quickly.

Migrating from SQLite to PostgreSQL in a Rails application is manageable, and tools like pgloader can help. Plan for it as a future milestone rather than a crisis.


FAQ

Can SQLite handle real production traffic in a Rails 8 app?

Yes, for the right workloads. If your application has a healthy mix of reads and writes and does not receive thousands of write requests per second, SQLite with WAL mode and proper configuration can handle production traffic reliably. Many apps that feel "busy" are not generating the write volume that would push SQLite past its limits.

Is SQLite safe to use in production, given that it is a file?

Safety comes from your backup strategy, not your database engine. With Litestream providing continuous replication to object storage, SQLite can be as safe as any managed database. The key is having a tested restore process before you need it.

Does Solid Queue work well with SQLite?

Yes, and Rails 8 is specifically designed for this combination. The recommended approach is to use a separate SQLite database for Solid Queue so it does not compete with your primary application database for write locks.

How do I know if I have write contention problems?

Watch for SQLite3::BusyException in your logs and error tracking tools. You can also monitor the Solid Queue dispatcher metrics and look for high queue latency. A large number of retried jobs is another signal.

What is the difference between timeout in database.yml and PRAGMA busy_timeout?

In Rails, the timeout key in database.yml for the SQLite3 adapter maps directly to PRAGMA busy_timeout. They control the same thing. The timeout key in database.yml is the more idiomatic Rails way to set it, while using the PRAGMA directly in an initializer gives you more control and clarity.

Can I use multiple SQLite databases in one Rails 8 app?

Yes. Rails 8 supports multiple databases through Active Record's multi-database features. This is actually a recommended pattern: one database for your primary data, one for the queue, and one for the cache. Each database has its own connection pool and can be tuned independently.

Do I need Redis at all if I use Rails 8 with SQLite?

No. Rails 8 with Solid Queue (jobs), Solid Cache (caching), and Solid Cable (WebSockets) eliminates the need for Redis in most applications. This is one of the core design goals of the Rails 8 release.


Conclusion

SQLite is no longer the database you graduate away from. For a large class of Rails 8 applications, it is a deliberate and mature production choice.

The key to making it work is configuration. Enable WAL mode so reads and writes can happen concurrently. Set a busy timeout so write contention does not immediately crash your app. Use separate SQLite databases for your queue and cache. Tune your PRAGMA settings. Set up continuous backups with Litestream.

Done right, a single-server Rails 8 application with SQLite can handle significant traffic, cost very little to operate, and be simple enough to maintain solo. That combination is genuinely rare and worth taking seriously.

Start with the database.yml and initializer configurations from this guide, get Litestream running before you launch, and monitor write contention as your traffic grows. You will likely be surprised by how far SQLite takes you.

If you are starting a new Rails 8 project or simplifying an existing one, give SQLite in production a serious look. The defaults are better than ever, and the community knowledge around this pattern is growing quickly.

💌 Don’t miss out! Join my newsletter for web development tips, tutorials, and insights delivered straight to your inbox.

Thanks for reading & Happy coding! 🚀

Follow me on:

Code. Learn. Grow.

A friendly newsletter sharing dev tips, lessons, and wins from my journey.

    Services Tailored to Your Needs


    coding

    Web & Mobile Development

    Custom websites and mobile apps built to be fast, modern, and user-friendly. From sleek landing pages to full-scale applications, I deliver solutions that engage your audience and grow your business.

    API development

    Seamlessly connect your systems with secure, scalable APIs. I design and integrate APIs that improve efficiency, reliability, and flexibility for your business processes.

    Database design and management

    Reliable database solutions tailored to your needs. I design, optimize, and maintain databases that ensure performance, security, and scalability for your applications.

    You might also like…

    Master Ruby and Rails Challenges: From Overwhelmed to Pro
    Web Development

    Master Ruby And Rails Challenges: From Overwhelmed To Pro

    By Jean Emmanuel Cadet
    Published on: Jan 08, 2026
    Rails link_to vs button_to: When Should You Use Each?
    Web Development

    Rails Link_to Vs Button_to: When Should You Use Each?

    By Jean Emmanuel Cadet
    Published on: May 23, 2026
    AI Coding Assistants: How to Use Them Smartly
    Web Development

    AI Coding Assistants: How To Use Them Smartly

    By Jean Emmanuel Cadet
    Published on: Oct 01, 2025
    CodeCurious

    Designed for those who view software as architecture and code as literature.

    Legal
    Terms & Conditions Privacy Policy Disclaimer

    CodeCurious © 2025 - 2026. All rights reserved. | Made with ♥ by @jecode93