An Interactive Cost Comparison of DigitalOcean and AWS

Cloud infrastructure is becoming more and more of a commodity, but my AWS bill is not getting any less expensive. Can DigitalOcean’s SMB-focused cloud change that? I built an interactive calculator to find out.

What are the differences between DigitalOcean and AWS?

Public cloud platforms sell managed infrastructure as a service, offering pay-as-you-go pricing for access to virtual machines, storage, and more. In contrast to more traditional data centers, clouds provide software interfaces on top of their hardware, hiding away the data center’s operational details. Cloud customers pay a premium for this.

DigitalOcean logo

DigitalOcean’s sales pitch is that with their products, that premium is as small as possible. Instead of offering a large suite of infrastructure products with complicated pricing models, DigitalOcean claims to offer only the cloud services your small to medium-sized business really needs. This saves your company money, both in operating cost and engineering time.

AWS logo

This pitch sets DigitalOcean apart from AWS. Like the other two big cloud providers (GCP and Azure), AWS targets large enterprises first. SMBs are welcome, but the platform’s complexity can put them off. With different pricing tables and offerings across 81 availability zones, there are hundreds of thousands of possible SKUs you could find on your AWS bill—and those SKUs add up.

What do DigitalOcean and AWS have in common?

Every public cloud offers the same core products. The market has determined that if you’re going to call yourself a cloud, you have to sell these services. The list includes:

AWSAzureGCPDigitalOcean
Virtual MachinesEC2Virtual MachinesCompute EngineDroplets
Function RuntimeLambdaFunctionsCloud RunFunctions
Managed DatabasesRDSManaged DatabasesCloud SQLManaged Databases
Binary Object StorageS3Blob StorageCloud StorageSpaces
Load BalancingALBLoad BalancersCloud Load BalancingLoad Balancers

AWS and DigitalOcean share another thing in common—they’re both growing rapidly, no matter what the economy has been doing. In fact, all four of the public clouds in the above table have recorded ~30% top-line revenue growth, year-over-year, for the last several years. Whether you’re catering to large enterprises or SMB, it pays to be a cloud.

But can SMBs really save some money by going with DigitalOcean instead of AWS?

Comparing cloud costs using a reference architecture

It’s not trivially easy to directly compare cloud costs across providers. Despite offering the same core products, each cloud has different pricing models, quantifying and bundling services together in different ways. It’s hard to get your head around exactly what an app might cost to run without building out a spreadsheet.

This blog post contains a calculator that’s basically a beautified version of that spreadsheet. In an attempt to contextualize the real-world cost differences of hosting different flavors of software applications across these two platforms, I’ve designed a generalized reference architecture for cloud-hosted software applications.

Reference architecture diagram

Many apps fit into this general architecture. Different apps leverage the core services to varying degrees and in different ways—one app might rely heavily on VMs while another exclusively leverages the container runtime. For cloud billing purposes, all of their usage patterns fit into three categories—compute, storage, and data transfer.

If you squint at this diagram hard enough, you can probably squeeze any random company’s software architecture into it. With a hefty dose of imagination, then, here are three example applications from three fictional engineering teams, each of whom can see their product in this reference architecture.

AI video generator app

KubrickGPT logo

KubrickGPT’s platform lets our customers effortlessly generate high quality video and audio productions by inputting a series of text prompts into our web app, viewing the resulting video scene-by-scene as it is generated. Our users can even direct the movie as it is being generated by quickly shouting additional directorial instructions into our web interface. Once they’re satisfied, they can offer up their masterpieces for download out of our web backend with a single click.

Compute

All of that video generation requires a lot of dedicated compute power—for this, KubrickGPT relies on a large pool of high-powered virtual machines. This is the bulk of our compute workload—we don’t use a function runtime.

Storage

Our SQL database is pretty small—it’s mostly metadata for our users plus all the prompts that they’ve inputted over time. Our cloud storage footprint is large, though—we never delete our customers’ AI-generated videos unless they explicitly tell us to.

Transfer

We don’t have a ton of individual requests coming into our application’s load balancer, but we stream video out of it during production, and we also serve it out of object storage after it’s fully rendered. We transfer a lot of data out from our cloud.

Ad tech bidding backend

WhizBid logo

WhizBid is a programmatic advertising platform that bids on mobile video ads on our customers’ behalf. Our users log into our management console, upload their video ads, and select the demographic they’d like to target with their ad. Our bidding backend receives hundreds of thousands of ad bid requests per second, decides which ad slots are worth buying for our customers, and serves the ad after we’ve won the auction.

Compute

We use lots of big VMs to parse and interpret bid requests as they come in through our load balancer—that’s our core product functionality. Also, we run a lot of function containers. ETL jobs, backups, notifying users about events in our platform—you name it, we run it on our cloud’s function runtime. Lambda functions—we’re obsessed with them.

Storage

We have a pretty big SQL database, because we want a very high-powered instance running with lots of storage space beneath it so that we can serve as high a volume of concurrent queries as possible. We also store a lot of derived statistics in there, which we render in pretty graphs in our management console.

Transfer

Our platform receives about 500,000 bid requests per second. These HTTP requests are pretty small, and we usually respond with a very small payload, so our actual outbound data transfer through our load balancer is not very large. However, since we also serve our customers’ ads out of our backend, we incur significant cloud storage data transfer.

Marketing asset manager

LogoBase logo

LogoBase is an all-in-one platform for managing your company’s marketing assets. Our customers log into our UI and upload their company’s product logos. We transform them into many other formats and file sizes and serve the reformatted files out of our cloud backend. Developers love our service because we give them an easy-to-use JavaScript library that lets them load their logos without having to think about screen size in their front end code.

Compute

We run a small cluster of inexpensive VMs to serve the customer site, and use a lot of functions to asynchronously transform the uploaded logo files into different formats and image sizes. We also like function containers—but not as much as those weird ad tech guys down the street.

Storage

Our app has a pretty small database footprint, but we store a lot of files in object storage—that’s where we keep all the transformed logo images for rapid serving later on. Our customers never delete old creatives from their accounts, and we’re too scared to do it for them, so we have ended up with a significant object storage footprint.

Transfer

We do a fair amount of data transfer. We serve a lot of files, and they’re all images. Our customers want new logo changes to go live at a moment’s notice, but we don’t understand how to pull that off with a CDN, so we just don’t bother with caching. This means that we incur significant outbound object storage transfer.

The calculator

This calculator crunches the numbers to find out how much each of these three fictional businesses would spend per month running their apps in DigitalOcean and AWS. The three buttons at the top let you toggle between them. You can also grab the sliders for any of the individual cost drivers and change the values, to explore the costs of other usage patterns.


Compute (VMs)

VM Count: 25
Min CPUs per VM: 4
Min RAM per VM: 16GB

Compute (containers)

0.25 GB RAM
0.5 Seconds per container
20,000,000 Invocations/month

Storage

SQL DB footprint: 650GB
Object storage: 6,000GB
Container registry: 300GB

Transfer

Max LB usage: 135,000 reqs/sec
Data out (VMs): 10,000GB/month
Data out (obj. storage): 10TB/month

Cost driverAWSDigitalOcean
Compute (VMs)$3,656$3,150
Compute (Containers)$39$45
Storage$2,179$1,964
Data Transfer$1,887$258
Total Monthly Cost$7,761$5,417

Which cloud is cheaper?

As you click through the example apps and slide the numbers around, a pattern emerges. Even up against AWS’s cheapest region, DigitalOcean generally wins on operating cost. This becomes especially true for applications which do a lot of outbound data transfer. In many scenarios, AWS charges 9x as much for an equal amount of outbound transfer!

AWS vs DigitalOcean sketch

If you can get your software stack running on ARM, or if you run most of your workload in a function-style container runtime like Lambda, AWS might be a cheaper option for you. Their container runtime is priced slightly lower, and Graviton processors can give you around a 30% savings on compute.

Ultimately, though, these two platforms compete on another level, too: complexity. If your application can fit into DigitalOcean’s platform, and you’re never going to need all the software extras and mega-scalability that AWS can provide, it will probably be better for you to deploy in DigitalOcean. If you really need AWS’s bells and whistles, you’ll have to pay their prices.

How exactly does the calculator compute costs?

In order to avoid spending the rest of my life reading AWS billing documentation, I made a few assumptions.

DigitalOcean’s pricing tables are simple—there are a lot of AWS regions, and each one has different pricing tables. I decided to use prices from us-east-1 for this calculator. This is usually AWS’s cheapest region, so if DigitalOcean is still able to beat them on cost in that region, DigitalOcean isn’t likely even cheaper everywhere else.

I have also assumed that the entire architecture will be deployed in a single datacenter. Both DigitalOcean and AWS will end up charging extra for data transfer if you deploy your app across multiple data centers. However, AWS will charge you significantly more, so this also weighs the calculator in AWS’s favor.

ARM logo

Arguably AWS’s biggest product advancement recently is the development of their ARM-based Graviton machines. ARM chips use less electricity, and the majority of a data center’s operating cost is power. AWS passes the power savings on to its customers—if you choose to jump through the extra compilation hoops necessary to use their ARM machines, you save on compute.

DigitalOcean does not yet have an ARM-specific hardware offering which is comparable to AWS’s Graviton-based EC2 machines, so in order to preserve a more like-for-like comparison, I used pricing tables for EC2’s x86-based instance types.

I have done my best to compute these costs accurately, but I can’t think of a more transparent way to assert that than to let you inspect the code for yourself. To that end, here are the two Typescript classes the calculator uses to compute the line items in its output table:

DigitalOcean

class DigitalOceanCostCalculator {

    // general purpose Droplets
    // gb RAM, vCPUs, GB transfer, monthly cost
    generalPurposeVMs = [
        [8,   2,  4_000, 63.0],
        [16,  4,  5_000, 126.0],
        [32,  8,  6_000, 252.0],
        [64,  16, 7_000, 504.0],
        [128, 32, 8_000, 1008.0],
        [160, 40, 9_000, 1260.0],
    ];

    // general purpose PostgreSQL
    // disk, monthly cost
    postgreSQLs = [
        [25,  115],
        [60,  230],
        [145, 450],
        [325, 900],
        [695, 1_820],
        [850, 2_275],
    ];

    getMinVm(mem, cpu) {
        let minVm = this.generalPurposeVMs[0];
        for (let i = 1; i < this.generalPurposeVMs.length; i++) {
            if (minVm[0] >= mem && minVm[1] >= cpu) {
                break;
            }
            minVm = this.generalPurposeVMs[i];
        }
        return minVm;
    }

    getMinimalGenPurpVmMonthlyCost(mem, cpu) {
        return this.getMinVm(mem, cpu)[3];
    }

    getMinimalGenPurpVmTransferBudget(mem, cpu) {
        return this.getMinVm(mem, cpu)[2];
    }

    getMinimalPostgreSQLCost(footprint) {
        let min = this.postgreSQLs[0];
        for (let i = 1; i < this.postgreSQLs.length; i++) {
            if (min[0] >= footprint) {
                break;
            }
            min = this.postgreSQLs[i];
        }
        return min[1];
    }

    getVmComputeCost(state) {
        const minVmCost = this.getMinimalGenPurpVmMonthlyCost(state.minVmRAM, state.minVmCores);
        return minVmCost * state.vmCount;
    }

    getContainerComputeCost(state) {
        let gbSeconds = state.containerInvocationsPerMonth * state.containerRam * state.secsPerContainer;
        let billableGbSeconds = gbSeconds - 90_000;
        if (billableGbSeconds < 1) {
            return 0.0;
        }
        return billableGbSeconds * 0.0000185;
    }

    getStorageCost(state) {
        let cost = 0;

        // cost of SQL database
        cost = cost +  this.getMinimalPostgreSQLCost(state.sqlDbGbStored);

        // cost of object storage
        cost = cost + 5;
        let billableObjStorageGb = state.objGbStored - 250;
        if (billableObjStorageGb > 0) {
            cost = cost + (billableObjStorageGb * 0.02);
        }

        // cost of registry storage (pro plan)
        cost = cost + 20;
        let billableRegistryStorageGb = state.registryGbStored - 100;
        if (billableRegistryStorageGb > 0) {
            cost = cost + (billableRegistryStorageGb * 0.02);
        }

        return cost;
    }

    getTransferCost(state) {
        let cost = 0;

        // calculate how many load balancer nodes needed
        // assume cost driver is reqs per sec
        // (ignore simultaneous connections and new SSL connections limits per node)
        let lbNodesNeeded = 0;
        let remainingLbReqsPerSec = state.lbReqsPerSec;
        while (remainingLbReqsPerSec > 0) {
            remainingLbReqsPerSec = remainingLbReqsPerSec - 10_000;
            lbNodesNeeded = lbNodesNeeded + 1;
        }
        cost = cost + (lbNodesNeeded * 12);

        // add cost for outbound data transfer from object storage (first TB is free)
        let billableObjStorageOutboundTransferTB = state.objOutboundTbPerMonth - 1;
        if (billableObjStorageOutboundTransferTB > 0) {
            cost = cost + (billableObjStorageOutboundTransferTB * 10); // $0.01 / GiB (yes, 1/9th the cost of AWS)
        }

        // add cost for excess data transfer from VMs ($0.01 per GB of excess transfer)
        const transferBudgetPerVm = this.getMinimalGenPurpVmTransferBudget(state.minVmRAM, state.minVmCores);
        const totalTransferBudget = transferBudgetPerVm * state.vmCount;
        const billableVmTransfer = state.lbOutboundGbPerMonth - totalTransferBudget;
        if (billableVmTransfer > 0) {
            cost = cost + (billableVmTransfer * 0.01);
        }
        return cost;
    }

    getTotalCost(state) {
        return this.getVmComputeCost(state)
            + this.getContainerComputeCost(state)
            + this.getStorageCost(state)
            + this.getTransferCost(state);
    }
}

AWS

class AWSCostCalculator {

    // these are m5s (x86_64) in us-east-1 (hourly on-demand prices converted to monthly)
    // vCPUs, RAM, monthly cost
    vms = [
        [2,  8,   0.096 * 24 * 30],
        [4,  16,  0.192 * 24 * 30],
        [8,  32,  0.384 * 24 * 30],
        [16, 64,  0.768 * 24 * 30],
        [32, 128, 1.536 * 24 * 30],
        [48, 192, 2.304 * 24 * 30],
    ];

    // these are db.m7gs (ARM) in us-east-1 (hourly on-demand prices converted to monthly)
    // roughly matching to DOCN mem/cpu specs to derive a disk size from AWS instance sizes
    // unfortunately this is not always exactly a 1-to-1 comparison, but it's not too far off
    // cpu, ram, disk, monthly cost
    dbVms = [
        [2,  8,   25,  0.168 * 24 * 30],
        [4,  16,  60,  0.337 * 24 * 30],
        [8,  32,  145, 0.674 * 24 * 30],
        [16, 64,  325, 1.348 * 24 * 30],
        [32, 128, 695, 2.696 * 24 * 30],
        [48, 192, 850, 4.044 * 24 * 30],
    ];

    getMinCostPerVm(state) {
        let vm = this.vms[0];
        for (let i = 1; i < this.vms.length; i++) {
            if (vm[1] >= state.minVmRAM && vm[0] >= state.minVmCores) {
                break;
            }
            vm = this.vms[i];
        }
        return vm[2];
    }

    getMinDbVmCost(state) {
        let vm = this.dbVms[0];
        for (let i = 1; i < this.dbVms.length; i++) {
            if (vm[2] >= state.sqlDbGbStored) {
                break;
            }
            vm = this.dbVms[i];
        }
        return vm[3];
    }

    getVmComputeCost(state) {

        // cost for VMs
        let cost = this.getMinCostPerVm(state) * state.vmCount;

        // cost for EBS volumes - use gp3 SSDs (cheapest)
        // assume only 100GB needed
        // assume 3000 free IOPS are sufficient (very few VM disk reads/writes)
        let ebsCostPerVm = 0.08 * 100;
        cost = cost + (ebsCostPerVm * state.vmCount);

        return cost;
    }

    getContainerComputeCost(state) {
        // cost for first 7.5B invocations in us-east-1 on x86
        let gbSeconds = state.containerInvocationsPerMonth * state.containerRam * state.secsPerContainer;
        let billableGbSeconds = gbSeconds - 400_000;
        let cost = 0;
        if (billableGbSeconds < 1) {
            return cost;
        }
        cost = billableGbSeconds * 0.0000166667;
        cost = cost + (state.containerInvocationsPerMonth / 1_000_000 * 0.20);
        return cost;
    }

    getStorageCost(state) {
        let cost = 0;

        // sql database instance
        cost = cost + this.getMinDbVmCost(state);

        // sql database SSD (us-east-1 gp2)
        // boldly assume no IOPS or disk throughput costs over baseline
        cost = cost + (0.115 * state.sqlDbGbStored);

        // object storage (rate for first 50TB in us-east-1)
        cost = cost + (0.023 * state.objGbStored);

        // registry storage
        let billableRegistryStorage = state.registryGbStored - 50;
        if (billableRegistryStorage > 0) {
            cost = cost + (billableRegistryStorage * 0.10);
        }

        return cost;
    }

    getTransferCost(state) {
        let cost = 0;

        // load balancer
        // cost of 30 continuous days of ALB-hours in us-east-1
        cost = cost + (0.0225 * 24 * 30);

        // Assume LB outbound GB per month is processed bytes (this is generous to AWS)
        // cost of LCU-hours to support that bandwidth transfer
        cost = cost + (state.lbOutboundGbPerMonth * 0.008);

        // ec2 -> internet transfer cost
        let billableOutboundEc2Gb = state.lbOutboundGbPerMonth - 100;

        // additional TB out from us-east-1 costs $0.07/GB
        // (calculator won't let you go above 100TB)
        let highTierGb = Math.max(billableOutboundEc2Gb - 50_000, 0);
        cost = cost + (highTierGb * 0.07);
        billableOutboundEc2Gb = billableOutboundEc2Gb - highTierGb;

        // next 40 TB out from us-east-1 EC2 costs $0.085/GB
        let midTierGb = Math.max(billableOutboundEc2Gb - 10_000, 0);
        cost = cost + (midTierGb * .085);
        billableOutboundEc2Gb = billableOutboundEc2Gb - midTierGb;

        // first 10 TB out from us-east-1 EC2 costs $0.09/GB
        let firstTierGb = Math.max(billableOutboundEc2Gb, 0);
        cost = cost + (firstTierGb * 0.09);

        // outbound from s3 to internet
        let billableOutboundObjectStorageTb = state.objOutboundTbPerMonth;

        // additional TB out from us-east-1 S3 costs $0.07/GB
        let highTierTb = Math.max(billableOutboundObjectStorageTb - 50, 0);
        cost = cost + (highTierTb * 70);
        billableOutboundObjectStorageTb = billableOutboundObjectStorageTb - highTierTb;

        // next 40 TB out from us-east-1 S3 costs $0.085/GB
        let midTierTb = Math.max(billableOutboundObjectStorageTb - 10, 0);
        cost = cost + (midTierTb * 85);
        billableOutboundObjectStorageTb = billableOutboundObjectStorageTb - midTierTb;

        // first 10 TB out from us-east-1 S3 costs $0.09/GB
        let firstTierTb = Math.max(billableOutboundObjectStorageTb, 0);
        cost = cost + (firstTierTb * 90);

        return cost;
    }

    getTotalCost(state): number {
        return this.getVmComputeCost(state)
            + this.getContainerComputeCost(state)
            + this.getStorageCost(state)
            + this.getTransferCost(state);
    }
}

Do your part

All public clouds have differentiators, but clouds are commoditizing. GCP and Azure leverage their existing business relationships. DigitalOcean offers simplicity, and AWS offers complexity. At their core, though, every public cloud offers the same basic product. For many app architectures, it’s getting easier and easier to compare public cloud offerings on price.

Screenshot from Starship Troopers

Cloud platforms follow one of the most reliable business models in tech—build hardware, and write software that runs on that hardware to solve a specific customer need. The hardware gives them the moat, and the software gives them the margin. Whether it’s Apple’s iPhone or AWS’s EC2, this formula drives revenue growth.

As cloud services continue to commoditize, it will get easier and easier to compare your application’s hosting costs between them. Public cloud hosting is expensive, and ongoing cost competition is the only force that will drive down cloud prices over time. Do your part and pinch your cloud pennies—the next generation of internet startups will thank you.