A properly configured VPS handles ecommerce workloads at 60-80% lower cost than managed platforms while delivering sub-200ms response times. The key is layering nginx connection management, PostgreSQL index strategy, Redis session persistence, CDN cache headers, and rate limiting into a cohesive stack that survives traffic spikes without dropping carts or losing conversions.
This guide walks through every layer of a production-ready ecommerce VPS, with actual configuration files you can deploy today. For compute options and pricing tiers, see the Pricing page.
Server Sizing for Ecommerce Workloads
Ecommerce servers have a different resource profile than content sites or APIs. Product catalog queries are CPU-heavy, cart sessions demand memory, and image serving eats bandwidth. Here is a sizing table based on real-world stores:
| Store Size | Products | Concurrent Users | Recommended VPS | Monthly Cost |
| Small shop | Under 500 | 10-30 | 2 vCPU, 4GB RAM, 80GB SSD | $20-40/mo |
| Mid-size store | 500-5,000 | 30-150 | 4 vCPU, 8GB RAM, 160GB SSD | $40-80/mo |
| Large catalog | 5,000-50,000 | 150-500 | 8 vCPU, 16GB RAM, 320GB SSD | $80-160/mo |
| High-traffic | 50,000+ | 500+ | 16 vCPU, 32GB RAM, 640GB SSD | $160-320/mo |
The RAM allocation matters most. PostgreSQL needs buffer pool memory, Redis needs session storage, and nginx needs connection buffers. A 4GB VPS should allocate roughly 1.5GB to PostgreSQL, 512MB to Redis, and leave 2GB for the OS and application processes. Check available compute tiers on the Services Overview page.
Nginx Configuration for Ecommerce Traffic
The default nginx config handles about 500 concurrent connections before performance degrades. An ecommerce store during a flash sale can see 5,000-10,000 concurrent connections. This configuration handles that load on a 4-core VPS:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
keepalive_timeout 30;
keepalive_requests 1000;
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 4;
gzip_types text/plain text/css application/json
application/javascript text/xml image/svg+xml;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
upstream ecommerce_app {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
keepalive 32;
}
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=checkout:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=search:10m rate=20r/s;
server {
listen 80;
server_name shop.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name shop.example.com;
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location /static/ {
alias /var/www/shop/static/;
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
}
location /media/products/ {
alias /var/www/shop/media/products/;
expires 30d;
add_header Cache-Control "public";
try_files $uri.webp $uri =404;
}
location /api/checkout {
limit_req zone=checkout burst=10 nodelay;
proxy_pass http://ecommerce_app;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
location /api/search {
limit_req zone=search burst=30 nodelay;
proxy_pass http://ecommerce_app;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://ecommerce_app;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
location / {
proxy_pass http://ecommerce_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}
Key details: `worker_connections 4096` with `multi_accept on` lets each worker accept multiple connections simultaneously. The `keepalive 32` directive on the upstream block maintains persistent connections to your app server, eliminating TCP handshake overhead per request. Rate limiting zones protect checkout (5 requests/second per IP) and search (20 requests/second) independently.
PostgreSQL Index Strategy for Product Catalogs
Product catalog queries are the heaviest database operations in ecommerce. Without proper indexes, a catalog page with filtering can take 2-5 seconds on 10,000+ products. These indexes bring that under 50ms:
-- Composite index for category browsing with price sorting
CREATE INDEX idx_products_category_price
ON products (category_id, price, status)
WHERE status = 'active';
-- Full-text search index for product search
CREATE INDEX idx_products_search
ON products USING gin(to_tsvector('english', name
' '
description));
-- Partial index for active products only (smaller, faster)
CREATE INDEX idx_products_active
ON products (updated_at DESC)
WHERE status = 'active';
-- Index for SKU lookups (cart, checkout)
CREATE UNIQUE INDEX idx_products_sku ON products (sku);
-- Composite index for inventory checks
CREATE INDEX idx_inventory_product_warehouse
ON inventory (product_id, warehouse_id)
WHERE quantity > 0;
-- Order history queries
CREATE INDEX idx_orders_user_date
ON orders (user_id, created_at DESC);
The partial index `WHERE status = 'active'` is critical. If 40% of your products are archived or draft, the index is 40% smaller and every query against it is faster. The GIN index for full-text search handles search queries without bringing in Elasticsearch for stores under 100,000 products.
PostgreSQL configuration tuning for ecommerce workloads (add to `postgresql.conf`):
<h1>Memory (for 8GB RAM VPS)</h1>
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 32MB
maintenance_work_mem = 512MB
<h1>Write performance</h1>
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 2GB
<h1>Query planner</h1>
random_page_cost = 1.1
effective_io_concurrency = 200
default_statistics_target = 200
Redis for Cart Sessions and Product Caching
Redis eliminates database round-trips for the two most frequent ecommerce operations: cart management and product page rendering. A typical ecommerce site sees 10-50x more cart reads than writes, making Redis caching extremely effective.
<h1>Redis configuration for ecommerce (redis.conf)</h1>
maxmemory 512mb
maxmemory-policy allkeys-lfu
save 900 1
save 300 10
save 60 10000
tcp-keepalive 300
<h1>Store cart session (TTL 7 days)</h1>
redis-cli SET cart:user:12345 '{"items":[{"sku":"WIDGET-001","qty":2,"price":29.99}]}' EX 604800
<h1>Cache product data (TTL 1 hour)</h1>
redis-cli SET product:SKU-001 '{"name":"Widget","price":29.99,"stock":142}' EX 3600
<h1>Cache category page results (TTL 5 minutes)</h1>
redis-cli SET category:electronics:page:1:sort:price '[...product_ids...]' EX 300
<h1>Track real-time inventory counts</h1>
redis-cli DECRBY inventory:SKU-001 1
<h1>Rate limiting with sliding window</h1>
redis-cli SET ratelimit:checkout:192.168.1.1 1 EX 60 NX
The `allkeys-lfu` eviction policy is ideal for ecommerce. It keeps frequently accessed products (bestsellers) in cache while evicting rarely viewed items. This outperforms `allkeys-lru` because popular products get viewed in bursts (sale events), not just recently.
CDN Integration and Cache Headers
A CDN reduces page load time by 40-70% for users outside your server region. For ecommerce, the strategy is to cache static assets aggressively while ensuring product prices and inventory are always fresh.
| Content Type | Cache Duration | Cache-Control Header | CDN Behavior |
| Product images | 30 days | public, max-age=2592000 | Cache at edge |
| CSS/JS bundles | 1 year | public, max-age=31536000, immutable | Cache permanently |
| Product pages | 5 minutes | public, max-age=300, s-maxage=300 | Short edge cache |
| Cart/checkout | No cache | private, no-store | Always origin |
| API responses | Varies | private, max-age=0 | Never cache |
| Font files | 1 year | public, max-age=31536000, immutable | Cache permanently |
Add these headers in your nginx config for CDN compatibility:
location ~* \.(jpgjpeg png webp avif gif svg
ico)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
add_header Vary "Accept";
access_log off;
}
location ~* \.(css
js woff2 woff
ttf)$ {
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
Arxentra provides built-in CDN edge caching through its Cloud Computing infrastructure, eliminating the need to configure third-party CDN providers separately.
Rate Limiting for Flash Sales and Bot Protection
Flash sales create two problems: legitimate traffic spikes and bot-driven inventory hoarding. Rate limiting must be granular enough to block bots without rejecting real customers.
The nginx rate limiting zones defined earlier provide the first layer. For application-level rate limiting with Redis:
import redis
import time
r = redis.Redis(host="localhost", port=6379, db=1)
def check_rate_limit(user_id, action, max_requests, window_seconds):
key = f"ratelimit:{action}:{user_id}"
current = r.get(key)
if current and int(current) >= max_requests:
return False
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window_seconds)
pipe.execute()
return True
<h1>Usage: 5 checkout attempts per minute per user</h1>
if not check_rate_limit(user_id, "checkout", 5, 60):
return {"error": "Too many checkout attempts. Please wait."}
Rate limits by endpoint for a typical ecommerce store:
- Product browsing: 60 requests/minute per IP
- Search: 30 requests/minute per IP
- Add to cart: 20 requests/minute per user
- Checkout: 5 requests/minute per user
- Payment submission: 3 requests/minute per user
- Account creation: 3 requests/hour per IP
For more on infrastructure management and scaling options, see the Documentation.
Performance Monitoring and Alerts
Track these five metrics to catch ecommerce performance issues before they affect revenue:
Frequently Asked Questions
Q: How much traffic can a single VPS handle for ecommerce?
A: A properly configured 4 vCPU, 8GB RAM VPS handles 150-300 concurrent users with sub-500ms response times. This translates to approximately 500,000-1,000,000 page views per month. Beyond that, either vertically scale to 8+ cores or add a second application server behind a load balancer.
Q: Should I use managed database hosting or run PostgreSQL on the same VPS?
A: For stores under $50,000 monthly revenue, running PostgreSQL on the same VPS is cost-effective and simplifies backups. Above that threshold, a managed database (separate VPS or cloud service) provides automated failover and point-in-time recovery that justify the 2-3x cost premium.
Q: How do I handle inventory accuracy during flash sales?
A: Use Redis DECRBY for real-time inventory tracking with PostgreSQL as the source of truth. Check Redis inventory before adding to cart, then verify against PostgreSQL during checkout. This prevents overselling while keeping cart operations fast.
Q: Is CDN necessary for a small ecommerce store?
A: If more than 30% of your customers are outside your server region, yes. A CDN reduces image load times from 800ms to under 100ms for distant users. Even for local traffic, a CDN offloads 60-80% of bandwidth from your VPS, freeing resources for dynamic requests.
Q: What is the best caching strategy for product pages with dynamic pricing?
A: Cache the product page template for 5 minutes at the CDN edge, but fetch price and inventory data client-side via API calls that bypass the cache. This gives you 90% of the caching benefit while ensuring prices are always current. Use `stale-while-revalidate` headers for the best user experience.
Q: How do I migrate my existing ecommerce store to a VPS?
A: Start with a staging VPS that mirrors your production setup. Migrate the database first, then configure nginx and Redis, and finally switch DNS. The entire process takes 2-4 hours with zero downtime if you use DNS-based cutover. See the Contact page to get migration assistance from the Arxentra team.