This is article 1 of the AWS Service Encyclopedia: one entry per AWS service, each a professional-grade definition followed by a build you can actually run. We start with EC2 because almost everything else in the catalog is EC2 wearing a costume. Fargate runs your containers on EC2 capacity you never see. RDS is a managed database on an instance you do not administer. Lambda is a slice of an EC2 host with a 15-minute leash. Understand the primitive and the rest of the catalog stops being magic.
What it actually is
Amazon EC2 (Elastic Compute Cloud) rents you virtual servers by the second. You pick a hardware profile (the instance type), pick an operating system image (the AMI), and AWS gives you a running machine with a private IP inside your network, a root disk, and root access. That is the whole product. Everything else EC2 offers is a knob on top of "here is a computer, you have the keys."
The mental model the marketing pages bury: EC2 is not a service that runs your app. It is a service that hands you a bare Linux or Windows box and then gets out of the way. AWS operates the physical host, the hypervisor, the network fabric, and the power. You operate everything from the kernel up: patches, the web server, the process that crashes at 3am, the disk that fills. This division is the single most important thing to internalize about EC2, because it is exactly what services like Lambda and Fargate exist to erase. When people say "serverless," they mean "EC2 with the operating-system responsibility removed." The instance is still there. You just stopped being its sysadmin.
The model
An EC2 instance is assembled from four primitives, and you choose each one independently.
The instance type is the hardware profile: how many vCPUs, how much memory, what network and storage bandwidth, which CPU architecture. Names decode left to right. In m8g.large, the m is the family (general purpose), 8 is the generation, g is the processor (g for AWS Graviton, i for Intel, a for AMD), and large is the size. Extra letters carry meaning too: d adds local NVMe instance storage, n adds network bandwidth, flex marks a flex instance that delivers full performance most of the time at a lower price. The families you will meet most: t for cheap burstable boxes, m for balanced general purpose, c for compute-heavy, r for memory-heavy, and the accelerated families (p, g, trn, inf) for GPUs and ML silicon. The current Graviton4 generation (m8g, c8g, r8g) offers up to 3x the vCPUs and memory of the Graviton3 generation and runs, per AWS, up to 40 percent faster on databases and 45 percent faster on large Java apps than Graviton3.
The AMI (Amazon Machine Image) is the boot disk template: the OS and any preinstalled software. The root volume is usually an EBS disk, network-attached block storage that survives a stop but is deleted with the instance unless you say otherwise. And the placement: the Region and Availability Zone the instance lives in, which dictates latency and blast radius.
The contract AWS makes on all of this is narrow and worth knowing precisely. An instance runs on one physical host in one AZ. If that host dies, the instance dies with it, and AWS does not migrate it for you. Durability of your data is your problem, solved by putting the data on EBS (which is replicated within the AZ) or S3, and by running more than one instance across more than one AZ. Scaling is your problem too, solved by the EC2 Auto Scaling group, which watches a metric and launches or terminates instances to track it. That is the honest shape of EC2: raw, durable-if-you-architect-it compute, with no built-in high availability. The high availability is a pattern you build, and the last section of this article builds it.
When to use it, when not to
EC2 is the right answer when you need control the managed services will not give you: a specific kernel, a GPU driver, a long-running stateful process, a legacy app that assumes a real filesystem, or simply the cheapest possible compute at steady scale. It is the wrong answer when the operational burden of owning an operating system outweighs the control, which is most web workloads most of the time.
Here is the honest comparison against the three services people most often confuse with it.
Dimension | EC2 | AWS Fargate | AWS Lambda | Lightsail |
|---|---|---|---|---|
You manage | OS, patching, scaling | Container only | Function code only | Almost nothing |
Unit of compute | Instance (per second) | Task (per second) | Invocation (per ms) | Fixed monthly plan |
Max run time | Unbounded | Unbounded | 15 minutes | Unbounded |
Best for | Full control, steady load | Containers without node management | Spiky, event-driven, short | Predictable small apps |
Worst for | Teams who do not want to own an OS | Cheapest steady-state compute | Long or stateful jobs | Anything you will outgrow |
The pattern to remember: as you move from EC2 to Fargate to Lambda, you trade control for the disappearance of the operating system, and you trade steady-state cost efficiency for the ability to pay nothing when idle. EC2 wins on control and on cost-at-scale. It loses on everything to do with not wanting to run a server. Lightsail is a separate case: a fixed-price bundle of a small instance, storage, and transfer, aimed at people who want a predictable bill and a simpler console. It is a fine on-ramp and a known trap, because the day you need a real VPC layout, an Auto Scaling group, or a load balancer, you migrate out of it into plain EC2.
What it costs
EC2 has six ways to pay, and picking the wrong one is the most common way teams overspend on AWS.
On-Demand is the default: you pay by the second (with a one-minute minimum on Linux) for what you run, no commitment. It is the most expensive per hour and the right choice for unpredictable or short-lived work.
Savings Plans cut that bill in exchange for a commitment. You commit to a steady dollars-per-hour of usage for a 1-year or 3-year term. Compute Savings Plans are the flexible flavor: up to 66 percent off, applied automatically across any instance family, size, Region, OS, and tenancy, and they even cover Fargate and Lambda. This is the single highest-leverage cost lever on AWS for anyone running steady compute.
Reserved Instances are the older commitment model, tied to a specific instance configuration and Region. Savings Plans have largely superseded them for new commitments, but RIs still matter when you need a capacity reservation alongside the discount.
Spot Instances sell you spare capacity at up to 90 percent off On-Demand, with one catch: AWS can reclaim the instance with a two-minute warning when it needs the capacity back. Spot is transformative for fault-tolerant, interruptible work (batch jobs, CI, stateless web behind a load balancer) and a disaster for anything that cannot survive a sudden termination. Note that Spot is not stacked with Savings Plans; the Spot discount is already the discount.
Dedicated Hosts and Dedicated Instances put you on single-tenant hardware for compliance or for bringing your own per-socket software licenses. Capacity Reservations hold capacity in a specific AZ so a launch never fails for lack of hardware, and Capacity Blocks reserve clusters of GPU instances for a future window.
The dimension that surprises people is not the instance at all. It is the data transfer and the EBS. Traffic out to the internet and traffic across AZs both bill per GB, an idle instance still pays for its EBS volume, and an Elastic IP that is not attached to a running instance bills by the hour. The instance-hour is the number you shop on; the network and storage lines are the ones that show up unexpected.
On the free tier, the rules changed on July 15, 2025, and which set applies depends entirely on when your account was born. Accounts created before that date get the classic 12-month free tier: 750 hours a month of a t2.micro or t3.micro, and you can exceed it and get billed. Accounts created on or after that date get a different deal entirely: a 100 USD sign-up credit plus up to 100 more as you use services, a 6-month window (or until the credits run out), and a hard stop instead of overage billing. The newer accounts also get a wider list of eligible types (t3.micro, t3.small, t4g.micro, t4g.small, c7i-flex.large, m7i-flex.large) rather than just the two micro sizes. Check your account's creation date before assuming which regime you are in.
The limits that bite
The first quota you will hit is the vCPU limit. EC2 On-Demand quotas are counted in vCPUs, not instance count, and the default for the standard family group (A, C, D, H, I, M, R, T, Z) is 20 vCPUs per Region. That is enough for a handful of small instances and nothing more. The good news is that AWS raises it automatically as your usage grows, and you can request an increase in Service Quotas; the bad news is that a load test or an aggressive Auto Scaling group can hit the ceiling mid-scale-out and start throwing capacity errors. Instances in the pending, stopping, stopped, and hibernated states do not count against the quota, which matters for the warm pool we build below.
The second is a design limit, not a number: an instance lives in exactly one AZ and does not move. AWS will not evacuate your instance off a failing host. If you run a single instance, you have signed up for its downtime. Availability is a multi-AZ Auto Scaling pattern, not a setting.
The third bites the t family specifically. Burstable instances earn CPU credits when idle and spend them to burst above a baseline. Run out of credits and, in the default "unlimited" mode, the instance keeps bursting and bills you for the surplus CPU. A t3.micro under sustained load can quietly cost more than a right-sized m instance while performing worse. Burstable is for bursty; it punishes steady load.
Build it
We are going to build the thing the plan promised: a right-sized instance behind an Auto Scaling group, with a target-tracking policy that adds instances under load and a warm pool of pre-initialized instances that cut scale-out latency. This is the minimum viable "EC2 done right" and it is almost entirely CLI, which is how AWS documents the Auto Scaling workflow.
Prerequisites. The AWS CLI v2 configured with credentials, a default VPC in your Region (every account has one unless you deleted it), and permission to create the resources. A representative IAM policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"ec2:CreateLaunchTemplate", "ec2:DescribeSubnets", "ec2:DescribeImages",
"ec2:RunInstances", "ec2:DescribeInstances", "ec2:DeleteLaunchTemplate",
"autoscaling:CreateAutoScalingGroup", "autoscaling:PutScalingPolicy",
"autoscaling:PutWarmPool", "autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeWarmPool", "autoscaling:DescribePolicies",
"autoscaling:DeleteAutoScalingGroup", "autoscaling:DeleteWarmPool",
"cloudwatch:PutMetricAlarm", "cloudwatch:SetAlarmState",
"iam:CreateServiceLinkedRole"
],
"Resource": "*"
}]
}
The iam:CreateServiceLinkedRole is there because Auto Scaling creates a service-linked role on first use. Estimated cost of the whole exercise: one or two t3.micro instances for under an hour, plus a stopped warm-pool instance that pays only for its EBS volume. Well under one US dollar if you clean up when done.
Step 1: Resolve a current AMI and your subnets. Never hardcode an AMI ID; they are Region-specific and they change. Pull the latest Amazon Linux 2023 image from its public SSM parameter, and grab two subnet IDs from your default VPC so the group can span two AZs.
AMI=$(aws ssm get-parameters \
--names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query 'Parameters[0].Value' --output text)
SUBNETS=$(aws ec2 describe-subnets \
--filters "Name=default-for-az,Values=true" \
--query 'Subnets[0:2].SubnetId' --output text | tr '\t' ',')
echo "AMI=$AMI SUBNETS=$SUBNETS"
Step 2: Create a launch template. The launch template is the recipe every instance in the group is stamped from: the AMI, the type, and a user-data script. We pick t3.micro because it is free-tier eligible under both the old and new regimes, and we bake in a trivial web server so a launched instance does something visible.
USERDATA=$(printf '#!/bin/bash\ndnf install -y httpd\nsystemctl enable --now httpd\necho "hello from $(hostname)" > /var/www/html/index.html\n' | base64)
aws ec2 create-launch-template \
--launch-template-name sl-ec2-demo \
--launch-template-data "{
\"ImageId\":\"$AMI\",
\"InstanceType\":\"t3.micro\",
\"UserData\":\"$USERDATA\"
}"
Step 3: Create the Auto Scaling group across two AZs. Minimum 1, maximum 4, desired 1. Spanning two subnets is what makes this survive an AZ failure: if one AZ goes dark, the group relaunches the instance in the other.
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name sl-ec2-asg \
--launch-template LaunchTemplateName=sl-ec2-demo,Version='$Latest' \
--min-size 1 --max-size 4 --desired-capacity 1 \
--vpc-zone-identifier "$SUBNETS" \
--health-check-type EC2 --health-check-grace-period 120
Step 4: Attach a target-tracking scaling policy. This is the whole point of an ASG. We tell it to keep average CPU across the group at 50 percent. Below that it scales in, above it scales out, and AWS creates and manages the CloudWatch alarms for us.
cat > tt-config.json <<'EOF'
{
"TargetValue": 50.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization"
}
}
EOF
aws autoscaling put-scaling-policy \
--auto-scaling-group-name sl-ec2-asg \
--policy-name cpu50-tt \
--policy-type TargetTrackingScaling \
--target-tracking-configuration file://tt-config.json
Step 5: Add a warm pool. A warm pool keeps pre-initialized instances in a Stopped state next to the group, so a scale-out event is a fast "start" instead of a slow "launch, boot, configure." Stopped instances pay only for their EBS volumes and do not count against your vCPU quota. We keep at least one warm.
aws autoscaling put-warm-pool \
--auto-scaling-group-name sl-ec2-asg \
--pool-state Stopped \
--min-size 1
Verify it works
Give the group two or three minutes, then confirm three things. First, the group launched one healthy instance:
aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-names sl-ec2-asg \
--query 'AutoScalingGroups[0].Instances[].[InstanceId,LifecycleState,HealthStatus]' \
--output table
You want to see one instance in InService and Healthy. Second, the warm pool has a stopped instance waiting:
aws autoscaling describe-warm-pool \
--auto-scaling-group-name sl-ec2-asg \
--query 'Instances[].[InstanceId,LifecycleState]' --output table
You want an instance in a Warmed:Stopped state. Third, the scaling policy exists and owns its alarms:
aws autoscaling describe-policies \
--auto-scaling-group-name sl-ec2-asg \
--query 'ScalingPolicies[].[PolicyName,PolicyType]' --output table
To actually watch it scale out without generating real load, force the high alarm the policy created into ALARM state. Find the alarm name in the describe-policies output under Alarms, then:
aws cloudwatch set-alarm-state \
--alarm-name <the-AlarmHigh-name-from-describe-policies> \
--state-value ALARM --state-reason "manual scale-out test"
Within a minute or two, describe-auto-scaling-groups will show the desired capacity climb and the warm-pool instance move into service. That warm start, seconds instead of a full boot, is the payoff.
When it breaks
If create-auto-scaling-group returns an AccessDenied mentioning a service-linked role, your principal cannot create it. Either add iam:CreateServiceLinkedRole or have an admin create AWSServiceRoleForAutoScaling once; after that the error disappears for everyone.
If instances launch and immediately terminate in a loop, the health check is failing. With --health-check-type EC2 the instance only needs to pass the EC2 system checks, so a crash loop usually means the AMI or user data is broken; check the instance's system log. If you later attach a load balancer and switch to ELB health checks, an instance that boots fine but fails the target-group health check will also loop, and the fix is almost always the grace period being too short for your app to start.
If scale-out never happens even under real load, the usual cause is the vCPU quota. Four t3.micro instances is 8 vCPUs, comfortably under the default 20, but if you scaled the max size up or you are already running other instances, you can hit the ceiling and see VcpuLimitExceeded. Check Service Quotas for "Running On-Demand Standard instances" and request an increase.
If the warm pool stays empty, remember its default size is max-size minus desired-capacity. With max 4 and desired 1 you get room for three warm instances, but we pinned --min-size 1 so at least one is always kept. If you set max-size equal to desired-capacity, the pool has nowhere to grow and stays empty by design.
Cleanup
Delete in the right order and the bill goes to zero. The --force-delete on the group tears down the warm pool and the instances with it.
aws autoscaling delete-auto-scaling-group \
--auto-scaling-group-name sl-ec2-asg --force-delete
aws ec2 delete-launch-template --launch-template-name sl-ec2-demo
Give it a minute, then confirm nothing is left running:
aws ec2 describe-instances \
--filters "Name=tag:aws:autoscaling:groupName,Values=sl-ec2-asg" \
"Name=instance-state-name,Values=running,stopped" \
--query 'Reservations[].Instances[].InstanceId' --output text
An empty result means every instance, including the stopped warm-pool one, is gone and you are paying nothing. Target-tracking alarms are deleted with the group. If you created an Elastic IP or an extra EBS volume by hand during experimentation, delete those separately; they bill even when nothing is running.
That is EC2 in one sitting: a computer you fully control, six ways to pay for it, the quotas and design limits that catch people, and a real scale-out architecture built and torn down for less than a dollar. Every managed service later in this series is a decision to give some of this control back in exchange for not owning the operating system. Now you know exactly what you are giving up.

