Olúṣeun

Multithread
Fast Apps Aren't Built, They're Scheduled



I'm currently building RiiMail, a desktop email client designed around a simple idea: all your email accounts in one place, with AI features built directly into the experience.

The project actually started from something much smaller. We built an AI phishing detection system that could analyse incoming emails and flag suspicious messages. It was released as a Gmail add-on, but after seeing how useful it was, we started asking a different question:

Why not build on it as an email client and have these capabilities built in?

Quick app initialization

When building a user-facing application, the app's initialisation speed matters, and users are unforgiving of whatever it takes to get that to happen. Yet it's one of the most important considerations during development because it influences almost every architectural decision.

Every design choice eventually comes back to a simple question:

How quickly can the user get into the app and start using it?

That became the priority guiding our tradeoffs, design, and approach to everything.

What does a typical email app do?

An email client sounds straightforward until you think about everything that needs to happen before the user sees their inbox.

At a minimum, it needs to:

The first five are expected to happen during onboarding. That's not an easy feat.

Now imagine a user with ten years of email history spread across multiple folders. The application needs to pull all of that data, process it, store it, and make it searchable, all while still feeling responsive.

The obvious solution is to wait until everything is finished before opening the application.

The problem is that users hate waiting.

So the real question becomes:

How do you make the app load as quickly as possible while still ensuring the data is available and properly indexed for querying?

The architecture

RiiMail is built using the Tauri framework with Rust as its backend.

I chose Rust not just because it's a language I'm comfortable with, but because of the control it gives when dealing with heavy application initialisation and concurrent workloads.

At the core of this is Tokio.

Before going deeper into that, it’s worth understanding how IMAP is handled.

We use the async-imap crate, which provides asynchronous IMAP support. At the centre of it is the IMAP Session, which represents an authenticated connection to the mail server.

The Session is mutable and stateful. You can make it thread-safe by wrapping it in a Mutex and an Arc, but that comes with tradeoffs. If not carefully designed, you can end up with lock contention or retained locks that slow the entire system down in subtle ways.

In fact, our initial design did exactly that.

So instead of sharing the session everywhere, we changed the model completely and made the background worker the sole owner of it.

Keeping the main thread light

The basic principle became simple:

Keep the main thread as light as possible

The IMAP session lives exclusively inside a background worker.

The frontend and command handlers never touch it directly. Instead, they send commands to the worker via channels, and the worker executes them asynchronously.

Most operations are fire-and-forget.

The only exception is authentication.

During login, the relationship is reversed: the main thread waits for the worker to complete authentication and report back before proceeding to application initialisation.

Login Flow Diagram

The biggest bottleneck: email synchronisation

Our main command is FetchEmails, and it's by far the heaviest operation in the system.

This is not just "fetching emails". It includes:

And this raises a harder question:

At what point do we consider the application ready?

Loading the first page, not the entire mailbox

Taking advantage of Tauri’s event system, we changed the definition of "ready".

The user doesn't need their entire mailbox immediately.

They only need enough data to start interacting with the application.

So instead of waiting for full synchronisation, we set a threshold: 100 emails.

Once the worker has fetched and persisted at least 100 emails, it emits a signal back to the frontend.

At that point, the frontend is released to start querying SQLite and populating its cache.

Meanwhile, synchronisation continues in the background.

This means a user can start reading and searching emails while thousands of additional messages are still being downloaded and indexed.

The mailbox is still syncing, but the app is already usable.

Real-time progress updates

We use the same Tauri event system to stream synchronisation progress to the frontend.

As emails are fetched and written into SQLite, the backend emits progress updates continuously.

The frontend subscribes to these events and updates the UI in real time.

This keeps the system decoupled:

Neither needs to know how the other is implemented internally.

Final thoughts

What started as a simple goal, making the application open faster, ended up shaping almost every part of the backend architecture.

The solution wasn't making email synchronisation faster.

It was making it invisible from the user's perspective.

By moving expensive work into background tasks, streaming early usable data, and decoupling UI readiness from full synchronisation, we can run heavy operations concurrently without compromising responsiveness.

The user sees an application that opens quickly.

Behind the scenes, Rust, Tokio, IMAP workers, SQLite, and background tasks are doing a lot more work than they realise.