
Performance Optimization of a Serverless Full-Stack CRUD Application Using AWS Lambda
This research evaluates the performance of a full-stack CRUD application using React, Node.js, PostgreSQL, AWS Lambda, API Gateway, and RDS. Using k6 load testing, the application was tested from 1 to 500 concurrent users. The study analyzes latency, throughput, error rates, and scalability, identifying performance bottlenecks and optimization opportunities in serverless architectures.
Introduction
Serverless architectures have become a popular approach for building scalable web applications without managing traditional application servers. AWS Lambda allows developers to run backend code without provisioning or maintaining servers, while Amazon API Gateway provides an HTTP interface for exposing Lambda functions.
However, moving a full-stack application from a traditional server-based architecture to a serverless architecture does not automatically guarantee better performance.
In this project, I explored the performance of a full-stack CRUD application by comparing a traditional local deployment with a serverless AWS deployment.
The application was built using:
- React.js
- Node.js
- Express.js
- PostgreSQL
- AWS Lambda
- Amazon API Gateway
- Amazon RDS for PostgreSQL
- Docker
- k6
The main goal was to understand:
How does a serverless full-stack CRUD application behave as the number of concurrent users increases?
1. Project Architecture
I started with a conventional full-stack architecture.
Local architecture
React │ ▼Node.js + Express.js │ ▼PostgreSQLThe React frontend communicates with the Express.js backend, which performs CRUD operations against PostgreSQL.
The application supports:
- Create task
- Read tasks
- Update task
- Delete task
- Mark task as completed
After establishing the local application, I created a serverless version.
AWS architecture
┌──────────────┐ │ React │ └──────┬───────┘ │ HTTPS │ ▼ ┌──────────────────┐ │ API Gateway │ │ HTTP API │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ AWS Lambda │ │ Node.js │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ RDS PostgreSQL │ └──────────────────┘The serverless implementation removes the continuously running Express server from the request path.
2. Why AWS Lambda?
The main reason for moving the backend to Lambda was to investigate the behavior of a serverless architecture under increasing workloads.
With a traditional backend, the application server is continuously running and handles incoming requests.
With Lambda, AWS manages the execution environment.
The simplified request flow becomes:
Client ↓API Gateway ↓Lambda ↓PostgreSQLThis architecture has several potential advantages:
- No server management
- Automatic scaling
- Pay-per-use execution model
- Easy integration with other AWS services
- Suitable for event-driven applications
However, serverless systems also introduce new performance considerations.
For example:
- Cold starts
- Lambda concurrency
- Database connection management
- API Gateway latency
- RDS connection limits
- Lambda memory configuration
These factors became important during my testing.
3. Building the CRUD Backend
The initial application was implemented using Node.js and Express.js.
A typical CRUD endpoint structure was:
GET /api/tasksPOST /api/tasksPUT /api/tasks/:idDELETE /api/tasks/:idFor example:
GET /api/tasksretrieves tasks from PostgreSQL.
The POST operation creates a task:
POST /api/tasksThe PUT operation updates an existing task:
PUT /api/tasks/10And DELETE removes it:
DELETE /api/tasks/10The same CRUD functionality was later implemented inside the Lambda function.
4. Moving PostgreSQL to Amazon RDS
The next step was moving the database from the local environment to Amazon RDS for PostgreSQL.
The database contained a simple
tasks table:CREATE TABLE tasks ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, completed BOOLEAN DEFAULT FALSE);A test record could be inserted using:
INSERT INTO tasks(title, description, completed)VALUES( 'AWS Lambda Test', 'Testing serverless backend', false);The Lambda function then connected to the RDS database using environment variables such as:
DB_HOSTDB_PORTDB_USERDB_PASSWORDDB_NAMEThis allowed the application code to remain independent of the actual database credentials.
5. Connecting API Gateway and Lambda
After creating the Lambda function, I exposed it through Amazon API Gateway.
The routes were configured as:
GET /api/tasksPOST /api/tasksPUT /api/tasks/{id}DELETE /api/tasks/{id}The request flow became:
HTTP Request ↓API Gateway ↓Lambda ↓PostgreSQL ↓Lambda ↓API Gateway ↓HTTP ResponseI tested the API using
curl before connecting it to the React frontend.For example:
curl https://YOUR_API_ENDPOINT/api/tasksThis helped isolate backend and networking problems before introducing frontend-related issues.
6. Why I Used k6
Once both implementations were working, the next challenge was measuring performance.
Instead of manually sending requests, I used k6 for automated load testing.
The test simulated multiple virtual users, commonly called VUs.
The initial workloads included:
1 VU10 VUs50 VUs100 VUs500 VUsEach test ran for approximately:
30 secondsThe CRUD test performed:
GET ↓POST ↓PUT ↓DELETEThis created a more realistic workload than testing only a single endpoint.
7. Lambda Baseline: 1 VU
The first Lambda test used one virtual user.
The results were:
Operation | Average | P95 |
|---|---|---|
GET | 444.35 ms | 795.31 ms |
POST | 307.91 ms | 358.26 ms |
PUT | 312.96 ms | 387.39 ms |
DELETE | 319.27 ms | 429.01 ms |
The overall HTTP request performance was:
Average: 346.12 msP95: 466.88 msError: 0%All configured thresholds passed.
This provided the baseline for the serverless implementation.
One interesting observation was that GET had noticeably higher latency than POST, PUT, and DELETE.
8. Increasing the Workload
The next step was gradually increasing the number of virtual users.
The purpose was not simply to find the fastest configuration.
Instead, I wanted to identify the point where the system began to degrade.
The general progression was:
1 VU │ ▼10 VUs │ ▼50 VUs │ ▼100 VUs │ ▼500 VUsAt lower concurrency levels, the Lambda application performed successfully.
As the workload increased, latency and errors began to increase.
9. The 500 VU Stress Test
The most interesting result came from the 500-VU test.
The test configuration was:
Virtual Users: 500Duration: 30 secondsThe test generated:
7,966 HTTP requests233.18 requests/secondHowever, the error rate increased significantly.
HTTP request failure rate: 78.16%CRUD error rate: 78.16%Only:
20.93%of all checks succeeded.
This was a major change compared with the 1-VU baseline.
10. CRUD Performance at 500 VUs
The individual CRUD results were:
Operation | Average | P95 | Maximum |
|---|---|---|---|
GET | 1.60 s | 3.31 s | 6.80 s |
POST | 1.44 s | 3.33 s | 3.73 s |
PUT | 1.47 s | 3.33 s | 3.64 s |
DELETE | 1.48 s | 3.28 s | 3.42 s |
The configured P95 threshold was:
P95 < 1 secondAll four CRUD operations failed this threshold at 500 VUs.
The overall HTTP results were:
Average: 1.51 secondsMedian: 1.16 secondsP90: 3.21 secondsP95: 3.31 secondsMaximum: 6.80 seconds
11. What the Results Tell Us
One of the most important lessons from this experiment is:
Higher throughput does not necessarily mean better application performance.
At 500 VUs, the system generated approximately:
233 requests/secondHowever, a large percentage of those requests failed.
Therefore, simply measuring requests per second would not provide an accurate picture of application health.
For this reason, I considered multiple metrics:
- Request throughput
- Average latency
- P95 latency
- Error rate
- Successful checks
- CRUD-specific latency
This provides a much more complete performance profile.
12. Local vs Serverless Testing
The same testing methodology was used for the local implementation.
The local architecture was:
React ↓Express.js ↓PostgreSQLwhile the AWS implementation was:
React ↓API Gateway ↓Lambda ↓RDS PostgreSQLThe purpose of the comparison is not simply to determine which architecture is "faster."
Instead, the research asks how the two architectures behave as workload increases.
The comparison should consider:
Metric | Local | Lambda |
|---|---|---|
Throughput | Measured by k6 | Measured by k6 |
Average latency | Measured by k6 | Measured by k6 |
P95 latency | Measured by k6 | Measured by k6 |
Error rate | Measured by k6 | Measured by k6 |
CRUD success rate | Measured by k6 | Measured by k6 |
Maximum stable VUs | Determined experimentally | Determined experimentally |
The important point is that the local measurements should come directly from the corresponding k6 output rather than being estimated.
13. What Could Be Causing the Degradation?
The 500-VU results show a clear performance bottleneck, but the k6 output alone does not identify the exact cause.
Several components could contribute:
500 VUs │ ▼API Gateway │ ▼Lambda concurrency │ ▼Node.js execution │ ▼PostgreSQL connections │ ▼RDS capacityPotential bottlenecks include:
Lambda concurrency
A large number of simultaneous requests can result in many Lambda executions.
Database connections
This is particularly important.
Lambda functions can create many concurrent database connections if connection pooling is not designed carefully.
RDS capacity
The database may become a bottleneck when many Lambda executions simultaneously access PostgreSQL.
Cold starts
New Lambda execution environments can introduce additional latency.
API Gateway
API Gateway is another component between the client and Lambda, so its contribution should also be considered.
14. Database Connection Management
One of the most important areas I plan to investigate is database connection management.
The Lambda implementation uses a PostgreSQL connection pool.
For example:
const { Pool } = require("pg");
let pool;
function getPool() { if (!pool) { pool = new Pool({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT || 5432), user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, max: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000 }); }
return pool;}The pool is created outside the Lambda handler and reused when the execution environment remains warm.
This is an important serverless optimization because database connections are relatively expensive resources.
15. Next Optimization Experiment
The next stage of the research is not to simply increase the number of VUs.
Instead, I want to investigate why performance degrades and determine which configuration changes improve it.
The next experiments can include:
Baseline │ ├── Lambda memory optimization │ ├── Database connection optimization │ ├── RDS capacity optimization │ ├── Lambda concurrency configuration │ └── RDS ProxyEach optimization should be tested independently.
For example:
Configuration ALambda → RDS
vs
Configuration BLambda → RDS Proxy → RDSThen the same k6 workload can be executed against both configurations.
16. CloudWatch Monitoring
k6 tells us what the user experiences.
AWS monitoring can help explain why it happens.
For Lambda, useful CloudWatch metrics include:
- Invocations
- Duration
- Errors
- Throttles
- Concurrent executions
For RDS, useful metrics include:
- CPU utilization
- Database connections
- Freeable memory
- Read/write IOPS
- Network throughput
- Database latency indicators
The experimental workflow therefore becomes:
k6 │ ├── Latency ├── Throughput ├── Errors └── Checks │ ▼ CloudWatch │ ├── Lambda └── RDSThis combination provides much stronger evidence for performance analysis.
17. Lessons Learned
This project taught me several important lessons about serverless application development.
1. Deployment is not optimization
Moving an application to Lambda does not automatically make it faster.
Performance depends on the entire architecture.
2. Database design matters
A serverless backend can generate many concurrent database operations.
Database connection management becomes especially important.
3. P95 is important
Average latency can hide slow requests.
For example, at 500 VUs the average HTTP latency was approximately:
1.51 secondsbut the P95 was:
3.31 secondsThis means a significant portion of users experienced much slower responses.
4. Stress testing reveals bottlenecks
The low-concurrency tests demonstrated that the system could operate successfully under light load.
The 500-VU test exposed the scalability limitations.
5. Monitoring is essential
Load-testing results alone cannot explain the exact AWS bottleneck.
Combining k6 with CloudWatch provides much better diagnostic information.
18. Final Architecture for the Research
The final experimental architecture is:
Internet │ ▼ ┌───────────────┐ │ React │ │ Frontend │ └───────┬───────┘ │ HTTPS │ ▼ ┌───────────────┐ │ API Gateway │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ AWS Lambda │ │ Node.js │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ PostgreSQL │ │ RDS │ └───────────────┘The performance testing layer sits outside this architecture:
┌───────────────┐ │ k6 │ │ Load Testing │ └───────┬───────┘ │ ▼ API Gateway │ Lambda → RDS
Conclusion
This project started as a simple full-stack CRUD application and evolved into a performance study of serverless architecture.
The initial implementation used:
ReactNode.jsExpress.jsPostgreSQLIt was then migrated to:
ReactAPI GatewayAWS LambdaRDS PostgreSQLUsing k6, I tested the serverless application under increasing levels of concurrency.
The low-concurrency experiments showed stable behavior with zero observed request failures. However, the 500-VU stress test produced a 78.16% HTTP request failure rate and a 3.31-second P95 HTTP latency, demonstrating significant degradation under high concurrency.
The next phase of the research is therefore focused on identifying the bottleneck rather than simply increasing the workload.
The planned experiments include Lambda configuration, database connection management, RDS capacity, concurrency behavior, and potentially RDS Proxy.
The overall goal is to answer a practical question:
How can a full-stack serverless CRUD application be optimized to maintain low latency and high reliability as concurrent workload increases?
This experiment has given me a baseline from which I can systematically measure the impact of each optimization.
The uploaded image shows how the frontend receives data using API Gateway and AWS Lambda to retrieve information from the RDS database.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article