Building a Go-based Image Delivery Platform
I fixed a huge issue with my website in just 8 hours. . .
Modern web apps rely heavily on content deliverability. The sad part is, real-world network conditions and bandwidth limitations make this an ideal, but not the rule. As such, deliverability is critical. I built a custom image delivery service to solve these very problems.
The bandwidth issue is very real. I pay for a gigabit connection from my internet service provier. When I run speed tests on a hardline connection, I get a respectable 700+ mbps. Yes, this lower than what I pay for, but others are using this network, it is to be expected. But when I run the same test on a mobile device, we see a vastly different story.

This test was run on a Google Pixel 6a, with a wifi 7 access point and a 2.5Gb uplink to the core router. The latency is not bad, that is a notible improvement over some networks, only 2ms slower than a hardlink connection. However, the speed is far more concerning. Where did it go? The real world is noisy. My neighbors are all broadcasting their own connections, drowning out my own. So, if the average user sees far below what they pay for, we must adapt.
This is the buisness model of many companies. Services like AWS, Cloudflare, Imgix, ImageKit, etc. are great. They offer a very competitive service, and some offer competitive pricing. However, many are very expensive. Plugging into them can be complicated, and you are now beholden to a third-party service provider for a core feature of your webservice. What happens when AWS or Cloudflare have their next big outage? Much of the internet just disappears. So I did it myself.
In the span of 8 hours, I managed to learn enough Go to write a decently responsive image proxy. Not only that, I made it a dockerized service. Now, this website does use third-party services. As of writing this, we use Cloudflare for DNS, DDOS, and WAF protection. If Cloudflare has a big outage, we will be affected. But not everything can be made yourself, and even if I did have the skills to recreate Cloudflare's performance and technology, the hosting for it alone would destroy my wallet. But services like Imgix are, in my opinion, over priced. I achieved a very low latency, high throughput docker container that easily attaches itself to an S3 compatible storage solution. You can host one yourself, use AWS, Digital Ocean, Cloudflare R2, Linode-Akami Storage, or dozens of others. Fundamentally, these buckets are all just web-servers. Streaming data from infrastructure to user. But pre-signed URLs do not last forever, and they open you up to bandwidth charges really quickly. If I made every asset on this website run directly off of the bucket, within a couple of days, some troll would find these signed URLs and hammer them. The image above is tiny, just about 20KB in size. But after 50 requests we've reached a MB, after 50 thousand requests we've reached 1GB. Using a service like Linode you could easily spin up a couple cheap $0.0075/hr VPS', then continuously curl the same URL over and over again, hundreds of times per second. Within a few hours, I'm broke.
So, we get to the reason this matters: performance. This website is only possible if I can afford it. Should someone find a way to get my S3 credentials, I'm doomed. Terabytes of storage, instantly accessible and able to be abused. We must also consider that users are often impatient. If a service takes too long to load, users simply go somewhere else. We saw earlier that WiFi connections make bandwidth disappear. A 1MB image can take 800+ ms to load. Not to mention that S3 storage isn't built for speed, it is built to be redundant. This is simply not acceptable or usable. With just a couple lines of code, you can easily spin up your own image proxy. Mine had a couple key design choices behind it:
- Fast - I need this to serve data quickly. I cannot have 500ms page prints
- Flexible - Can't hardcode every value into it
- Cheap - I don't want this to be a mental or financial burden.
With these principals in mind I set out on my mission. And I did it. We take some variables for the S3 bucket. Its secret key, endpoint, etc. Then after starting, it listens. I point the entire "img" subdomain to it. Requests are taken in and processed, then we check if we've already worked on this image before. If this is the first request, we fetch from S3, using the query in the get request to set resize and compression parameters. After we've done that, we save a few parameters into memory. We save the query, image dimensions, and image data into memory. The next time this image is requested, instead of spending 800ms serving a 5MB file, we can spend 50ms serving a 500kb image that we've already done the work for.
Now yes, the proxy I've created is basic. However, when you are your only client, the features are tailored to yourself. But the project is opensourced, freely usable and editable. Find the repo for it here
Comments