High Five Studio

July 2026

Why I Replaced Redis with SQLite for Croatian Session Storage

One developer's journey from a 2.4 GB Redis cluster to a simpler, cheaper SQLite solution for session storage

Why I Replaced Redis with SQLite for Croatian Session Storage

I remember the exact moment I questioned my session storage choices. I was staring at a Grafana dashboard showing my Redis cluster eating 2.4 GB of RAM for a Croatian e-commerce site that served maybe 3,000 active users. The monthly cloud bill for that Redis instance was more than my VPS hosting for the entire application. I asked myself: do I really need an in-memory database for storing small blobs of session data that expire in 30 minutes?

The answer was no. And the replacement was sitting in my database folder the entire time: SQLite.

The Croatian Hosting Reality Check

If you're running a website in Croatia, you know the infrastructure landscape is different from what Silicon Valley blog posts describe. Most Croatian developers host on small VPS plans from domestic providers or affordable Hetzner servers. You're not scaling to millions of concurrent users overnight. You're building something that works reliably for a few hundred or a few thousand customers.

Redis makes sense when you have multiple app servers and need a shared cache. But for a single-server setup—which covers most Croatian web projects—Redis adds operational overhead that simply isn't justified. You're maintaining a separate service, monitoring its memory usage, handling connection pools, and paying for RAM that sits mostly idle between session reads.

SQLite, on the other hand, is already there. Every PHP, Python, or Node.js application on a Linux VPS has SQLite available by default. No installation. No daemon to manage. No port to open in the firewall. The database file lives on the same disk as your application code.

Why Session Storage Is the Perfect Use Case for SQLite

Sessions are small, short-lived, and accessed by key. A typical session blob is 500 bytes to 4 KB. You're not running complex joins or full-text searches. You're doing exactly one operation: SELECT data FROM sessions WHERE id = ? or INSERT OR REPLACE INTO sessions.

The Performance Benchmark Nobody Talks About

I ran a simple benchmark on a standard Croatian VPS (2 vCPU, 4 GB RAM, NVMe SSD). I measured 10,000 session read operations using both Redis and SQLite with WAL mode enabled.

  • Redis: 2.3 milliseconds average latency
  • SQLite (WAL mode): 1.8 milliseconds average latency

Yes, SQLite was actually faster for this workload. The reason is simple: there's no network round trip. Redis runs as a separate process, often on a different port, which means TCP overhead. SQLite runs in-process, directly reading from the file system. For a single-server application, the local file access beats network I/O every time.

Memory Footprint Comparison

Redis kept 2.4 GB of RAM allocated for sessions that rarely exceeded 100 MB of actual data. SQLite uses exactly the disk space your data occupies plus some journal overhead. On a 50 GB VPS, that's essentially free.

How I Migrated: A Practical Walkthrough

The migration took me an afternoon. Here's exactly what I did, step by step.

Step 1: Create the Sessions Table

CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    data BLOB,
    expires_at INTEGER NOT NULL
);

CREATE INDEX idx_sessions_expires ON sessions(expires_at);

Notice the expires_at column as an integer. I store Unix timestamps. This makes cleanup trivial: DELETE FROM sessions WHERE expires_at < strftime('%s', 'now').

Step 2: Enable WAL Mode

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;

WAL mode is critical. It allows concurrent reads while a write is in progress. Without it, session reads would block during cleanup operations. With WAL, reads proceed without waiting.

Step 3: Rewrite the Session Handler

In my PHP application, the session handler became:

class SQLiteSessionHandler implements SessionHandlerInterface {
    private PDO $db;
    
    public function read(string $id): string {
        $stmt = $this->db->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires_at > ?'
        );
        $stmt->execute([$id, time()]);
        $row = $stmt->fetch();
        return $row ? $row['data'] : '';
    }
    
    public function write(string $id, string $data): bool {
        $stmt = $this->db->prepare(
            'INSERT OR REPLACE INTO sessions (id, data, expires_at) VALUES (?, ?, ?)'
        );
        return $stmt->execute([$id, $data, time() + 1800]);
    }
}

That's it. No Redis connection. No serialization concerns. The database handles everything.

The Unexpected Benefits

After the migration, I noticed improvements beyond the reduced RAM usage.

Backup Simplicity

Backing up sessions used to require Redis dump files or AOF logs. Now I just include sessions.db in my daily backup script. One file, atomic by default. If the server crashes, I restore the entire session state from the last backup.

Debugging and Inspection

Ever tried to peek into a Redis session store? You need redis-cli, then KEYS * (which blocks the server), then GET each key. With SQLite, I just run:

SELECT id, length(data), expires_at FROM sessions WHERE expires_at > strftime('%s', 'now');

I can see exactly what's stored, how large each session is, and when it expires. This made debugging a session corruption issue trivially easy last month.

Atomic Operations

Redis provides some atomic operations, but SQLite gives you full ACID transactions. I once needed to invalidate all sessions for a specific user across devices. With Redis, I would have iterated keys. With SQLite:

DELETE FROM sessions WHERE id LIKE 'user_123_%';

One transaction, atomic, guaranteed.

When You Should Still Use Redis

I'm not saying Redis is useless. If you're running a multi-server architecture with a load balancer, you need a shared session store. Redis excels there. Also, if your sessions contain large binary data like rendered page fragments, Redis's in-memory access is beneficial.

But for the vast majority of Croatian web projects—single-server or two-server setups—Redis is overkill. The complexity it introduces outweighs the marginal performance gain. SQLite handles 50,000 concurrent session reads per second on a modest VPS. Unless you're running the Croatian equivalent of Booking.com, that's enough.

The One Thing That Almost Broke It

I almost abandoned this approach because of a simple mistake: I forgot to enable WAL mode. With the default journal mode, every session write triggered a lock that blocked all reads. The application felt sluggish during high-traffic periods.

After two hours of head-scratching, I realized the problem. I added PRAGMA journal_mode=WAL; to my connection setup script, and the issue vanished. The lesson: always verify your SQLite pragmas in production.

A Note for Croatian Developers

I've seen many Croatian agencies and freelancers deploy applications with Redis because "that's what the tutorial said." The tutorial assumed a multi-server architecture. Your client's small webshop doesn't need it.

Consider the total cost of ownership. Redis requires:

  • Dedicated RAM
  • A monitoring dashboard
  • Connection pooling configuration
  • Security hardening (authentication, firewall rules)
  • Regular memory defragmentation

SQLite requires:

  • A file path
  • One SQL create statement

For a project that generates €200/month in revenue, the Redis overhead is ridiculous. SQLite gives you the same reliability with near-zero operational cost.

The Practical Takeaway

Don't blindly follow architecture patterns designed for companies with 50 servers. Evaluate your actual traffic, your hosting budget, and your maintenance capacity. For session storage on a single-server setup, SQLite isn't a compromise—it's the better tool.

Start your next Croatian project with SQLite for sessions. If you ever outgrow it, migrating to Redis later is straightforward because the session interface is the same. But you probably won't outgrow it. I haven't, and my traffic has tripled since the switch.

The best part? That 2.4 GB of RAM I freed up is now handling my database query cache and static file serving. My monthly cloud bill dropped by €45. And my session reads are faster than ever.