Architecting WordPress on AWS: EC2, RDS, and S3
A technical deep dive into the architecture decisions, performance tuning, and lessons learned from running WordPress ecommerce on AWS
A few months ago I wrote about building a WordPress ecommerce proof of concept on AWS. Several people asked for more technical detail on the architecture, the specific decisions I made, and the problems I encountered. This is that deep dive.
Architecture Overview
The system has four primary components, each mapped to an AWS service.
The application tier is a single EC2 t2.micro instance running Amazon Linux, Apache 2.4, PHP 5.6, and WordPress 4.2 with WooCommerce 2.3. The database tier is an RDS db.t2.micro instance running MySQL 5.6. Static assets live in an S3 bucket. A Memcached node on ElastiCache handles object caching.
Each component communicates over the VPC's private network. The EC2 instance is the only resource with a public IP address. The RDS instance, ElastiCache node, and S3 bucket are all accessible only from within the VPC (S3 access uses a VPC endpoint to avoid traversing the public internet).
Why Separate the Database
Running MySQL on the same EC2 instance as WordPress is simpler and cheaper. You skip the RDS cost entirely. For a proof of concept, that would have been reasonable.
I chose to separate them for three reasons.
First, independent scaling. The database and the application have different resource profiles. WordPress is CPU-bound during page rendering and network-bound when serving static files. MySQL is I/O-bound during complex queries and memory-bound for caching query results. Colocating them means they compete for the same pool of CPU, memory, and I/O bandwidth. Separating them lets each tier use resources optimized for its workload.
Second, operational benefits. RDS provides automated daily backups with configurable retention (I set mine to seven days). It handles minor version upgrades. It provides point-in-time recovery down to a five-minute granularity. Replicating these capabilities on a self-managed MySQL instance is possible but requires significant operational effort.
Third, fault isolation. If the application server crashes or needs to be rebuilt, the database is unaffected. I can terminate the EC2 instance, launch a new one, deploy WordPress, point it at the existing RDS instance, and the site comes back with all its data intact. This separation of stateless compute from stateful storage is a fundamental cloud architecture pattern.
Security Group Configuration
I set up three security groups.
The web security group allows inbound TCP on port 80 (HTTP) and port 443 (HTTPS) from anywhere, and port 22 (SSH) from my specific IP address. The database security group allows inbound TCP on port 3306 (MySQL) only from the web security group. The cache security group allows inbound TCP on port 11211 (Memcached) only from the web security group.
This layered approach means that even if someone compromises the web server, they can only reach the database and cache from that specific instance. Direct access from the internet to either the database or cache is impossible.
One mistake I made initially was opening SSH to 0.0.0.0/0, meaning the entire internet. My CloudTrail logs showed brute-force login attempts within hours. I tightened it to my IP address immediately. Lesson learned: never expose SSH to the world, even temporarily.
Performance Tuning
Out of the box, WordPress on a t2.micro instance was slow. Page load times were around three to four seconds, which is unacceptable for an ecommerce site. Here is what I did.
PHP opcode caching. PHP normally compiles source files to bytecode on every request. OPcache, bundled with PHP 5.5 and later, caches compiled bytecode in shared memory. Enabling it reduced page generation time by roughly 40 percent. The configuration was straightforward: a few lines in php.ini to enable OPcache and set the memory allocation to 64MB.
Object caching with Memcached. WordPress and WooCommerce generate many database queries per page load. A product listing page might trigger 50 or more queries. The W3 Total Cache plugin can cache the results of these queries in Memcached, so subsequent requests for the same data hit memory instead of the database. I deployed a cache.t2.micro ElastiCache Memcached node and configured W3 Total Cache to use it. Database load dropped dramatically, and average page load time dropped to about 1.2 seconds.
Static asset offloading. Serving images, CSS, and JavaScript from the EC2 instance wastes Apache processes on static file I/O. Moving these to S3 frees Apache to focus on dynamic PHP processing. The W3 Total Cache plugin handles the synchronization: when you upload a product image through WordPress, it automatically copies it to S3 and rewrites the URL.
Apache tuning. The default Apache configuration on Amazon Linux uses the prefork MPM with conservative settings. I increased MaxClients to 20 (aggressive for a t2.micro, but appropriate for a POC with limited traffic) and configured KeepAlive with a short timeout of 5 seconds. These changes improved concurrency handling without exhausting the instance's limited memory.
Cost Breakdown
After three months of running this setup, here is what it costs.
The EC2 t2.micro instance: free tier, so zero for the first year. The RDS db.t2.micro instance: also free tier. The ElastiCache cache.t2.micro node: this is not free tier eligible, costing approximately $13 per month. The S3 bucket: negligible for the small amount of data I store, under a dollar. Data transfer: minimal, also under a dollar.
Total monthly cost: approximately $14, almost entirely from the ElastiCache node.
If I were optimizing for cost, I would run Memcached on the EC2 instance itself instead of using ElastiCache. For a proof of concept with low traffic, the performance penalty of colocating the cache with the application is acceptable. But using ElastiCache let me learn the service, and the architecture is more realistic for a production deployment where you would want managed caching.
Lessons Learned
Latency is the hidden cost of distribution. Every network hop adds latency. Separating components across services improves scalability and fault tolerance but adds round-trip time to every interaction. For WordPress, where a single page load involves many database queries, the cumulative effect of network latency between EC2 and RDS is meaningful. Caching mitigates this, but it does not eliminate it.
The free tier is a brilliant onboarding strategy. AWS gives you enough free resources to build something real. Not a toy deployment with artificial constraints, but a legitimate architecture that you could scale to production. The educational value is enormous, and from Amazon's perspective, every student who learns on the free tier is a future customer who will specify AWS in their professional career. It is a deeply strategic investment.
Monitoring matters from day one. I set up CloudWatch alarms for CPU utilization on the EC2 instance, freeable memory on RDS, and cache hit ratio on ElastiCache. These alarms caught two issues I would have missed otherwise: a memory leak in a WordPress plugin that slowly consumed all available RAM, and a misconfigured cron job that ran expensive database queries every minute instead of every hour.
Infrastructure as code is not optional. I set up everything manually through the AWS console. If I had to recreate this environment, I would need to remember dozens of configuration details. Next time, I plan to use CloudFormation or Terraform to define the infrastructure declaratively. Being able to version-control your infrastructure and recreate it reliably is essential, even for a proof of concept.
What I Would Change
If I were doing this again, I would start with infrastructure as code from the beginning. I would add CloudFront in front of S3 for better global performance. I would implement HTTPS from day one using a certificate from Let's Encrypt or AWS Certificate Manager. And I would set up automated deployments instead of SSH-ing into the server and running WordPress updates manually.
This project remains one of the most educational things I have done in graduate school. The gap between understanding cloud computing theoretically and operating a real application on cloud infrastructure is wide, and bridging it has made me a better researcher.