Building a Streaming-Aware Video Caching Architecture in Flutter for Naberise

Published on July 13, 2026 6 min read

When building a social media application, caching images is almost effortless. There are plenty of mature solutions that download a file once, store it on disk, and reuse it whenever it’s needed.

Video, however, is a completely different problem.

Unlike images, videos are consumed while they’re still being transferred over the network. A user expects playback to begin almost instantly, can seek to arbitrary positions at any time, and may never watch the entire video. These characteristics fundamentally change what an efficient caching strategy looks like.

While working on Naberise, we needed a video caching architecture that could satisfy a few important requirements:

  • Videos should start playing immediately.
  • A video should never be downloaded more than once.
  • Cache management should be completely automatic.
  • The solution should work on both Android and iOS.
  • The architecture should remain independent of any specific media player implementation.

Instead of relying on a traditional file cache, we ended up building a streaming-aware caching pipeline.

This article focuses on the architectural decisions behind that solution rather than the production implementation itself.

Why Traditional Cache Managers Fall Short

Most generic cache managers follow a simple workflow:

Network

Download Entire File

Save to Disk

Consume Later

For images, this works perfectly.

For social media videos, it introduces an unnecessary delay because playback cannot begin until the download finishes (or at least until the caching library decides the file is ready). Video players, on the other hand, are designed around streaming.

The moment playback begins, bytes are already flowing from the server into the player. Waiting for a download to complete before playback defeats one of the biggest advantages of HTTP video streaming. For a scrolling social feed, startup latency is significantly more important than download completion.

Videos Behave Differently Than Images

One of the biggest differences is that video players rarely request an entire file in a single HTTP request.

Instead, they typically issue range requests.

Range: bytes=0-

or

Range: bytes=524288-

These requests allow the player to:

  • start playback immediately,
  • seek to any position,
  • resume interrupted streams,
  • buffer only what is currently needed.

Because of this behavior, a video cache cannot simply assume that every network response represents a complete file.

It needs to understand streaming.

The Architecture

Rather than downloading videos separately for caching, we decided to integrate caching directly into the streaming pipeline.

The architecture looks like this:

Video Player


Local HTTP Proxy
┌────────┴────────┐
│ │
▼ ▼
CDN Response Disk Cache

Instead of giving the video player the original CDN URL, it receives a localhost URL exposed by a lightweight HTTP proxy running inside the application.

Whenever the player requests a video:

  1. The proxy checks whether the video already exists in the local cache.
  2. If it exists, the proxy serves the video directly from disk.
  3. If it doesn’t exist, the proxy forwards the request to the CDN.
  4. Every byte received from the CDN is immediately forwarded to the player while simultaneously being written to disk.

This means playback and caching happen at exactly the same time.

The video is downloaded only once.

Streaming and Caching Simultaneously

The core idea behind the architecture is surprisingly simple. Rather than treating playback and caching as two independent operations, both become consumers of the same incoming stream.

Conceptually, it looks like this:

await for (final chunk in networkStream) {
player.write(chunk);
cache.write(chunk);
}

Every network chunk has two destinations:

  • the video player,
  • the disk cache.

This pattern is commonly known as teeing a stream. The important consequence is that there is never a second download whose only purpose is to populate the cache. The same bytes used for playback become the cached video.

Why a Local Proxy?

Using a local HTTP proxy may initially seem like an unusual design choice. We don’t control how the video player performs network requests.

What we can control is where those requests go.

Instead of modifying the player itself, we place a lightweight proxy between the player and the CDN.

final proxyUrl = proxyServer.proxyUrl(originalUrl);
VideoPlayerController.networkUrl(
Uri.parse(proxyUrl),
);

This gives the application full control over:

  • cache lookups,
  • streaming,
  • request forwarding,
  • response interception,
  • disk persistence,

without changing how the player itself behaves.

Managing the Disk Cache

Once videos start accumulating on disk, storage management becomes just as important as downloading them. Instead of treating the cache as an unlimited storage location, the cache is managed using a fixed size budget together with an LRU (Least Recently Used) eviction strategy.

Conceptually:

Video

Metadata

Last Access Time

LRU Eviction

Every cached video stores lightweight metadata describing:

  • its cache key,
  • file location,
  • size,
  • last access timestamp.

When the configured storage limit is reached, the least recently used videos are removed automatically. This keeps the cache size predictable without requiring any manual cleanup.

Production Considerations

Building a streaming-aware cache involves considerably more than simply writing files to disk. Several practical issues need to be handled to make the architecture reliable across platforms.

Some examples include:

  • Supporting HTTP Range Requests.
  • Correctly handling “206 Partial Content” responses.
  • Preventing duplicate downloads when multiple widgets request the same video simultaneously.
  • Using atomic file operations to avoid partially written cache entries.
  • Recovering safely from interrupted downloads.
  • Handling platform-specific HTTP quirks (particularly around partial responses).

None of these problems are especially difficult individually, but together they define whether a cache behaves reliably in production.

Trade-offs

Like most architectural decisions, this approach comes with both advantages and additional complexity.

┌──────────────────────────────┬────────────────────────────────┐
│ Benefits │ Trade-offs │
├──────────────────────────────┼────────────────────────────────┤
│ ✓ Instant playback │ More architectural complexity │
│ ✓ Single network download │ Local HTTP proxy │
│ ✓ Automatic disk caching │ Manual cache management │
│ ✓ Platform independent │ Additional streaming logic │
│ ✓ Better user experience │ More moving parts │
└──────────────────────────────┴────────────────────────────────┘

For applications where videos represent only a small portion of the experience, a generic cache manager may be entirely sufficient.

For a video-first social feed, however, the additional complexity proved worthwhile.

Final Thoughts

Video caching is fundamentally different from image caching. The challenge isn’t simply storing files on disk — it’s integrating storage into an active streaming pipeline without affecting playback.

By placing a lightweight local proxy between the player and the network, we were able to build a caching system that downloads each video only once, starts playback immediately, and works consistently across both Android and iOS.

More importantly, the architecture remains independent of any specific video player implementation, making it flexible enough to evolve alongside the application.

Today, Naberise has been downloaded more than 200,000 times and continues to grow with every new release.

As the application evolves, this architecture will continue evolving alongside it. There are always new optimizations to explore, new edge cases to solve, and new ideas to experiment with. This implementation reflects where we are today — not where we’ll stop.

Stay tuned, thanks for reading.

Building a Streaming-Aware Video Caching Architecture in Flutter for Naberise

Category: social-mediaTags: social-media, flutter, computer-science, software-architecture