AWS Builder Center
Performance Optimization of a Serverless Full-Stack CRUD Application Using AWS Lambda

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
│
▼
PostgreSQL
The 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
↓
PostgreSQL
This 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/tasks
POST /api/tasks
PUT /api/tasks/:id
DELETE /api/tasks/:id
For example:
GET /api/tasks
retrieves tasks from PostgreSQL.
The POST operation creates a task:
POST /api/tasks
The PUT operation updates an existing task:
PUT /api/tasks/10
And DELETE removes it:
DELETE /api/tasks/10
The 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_HOST
DB_PORT
DB_USER
DB_PASSWORD
DB_NAME
This 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/tasks
POST /api/tasks
PUT /api/tasks/{id}
DELETE /api/tasks/{id}
The request flow became:
HTTP Request
↓
API Gateway
↓
Lambda
↓
PostgreSQL
↓
Lambda
↓
API Gateway
↓
HTTP Response
I tested the API using  curl  before connecting it to the React frontend.
For example:
curl https://YOUR_API_ENDPOINT/api/tasks
This 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 VU
10 VUs
50 VUs
100 VUs
500 VUs
Each test ran for approximately:
30 seconds
The CRUD test performed:
GET
↓
POST
↓
PUT
↓
DELETE
This 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 ms
P95: 466.88 ms
Error: 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 VUs
At 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: 500
Duration: 30 seconds
The test generated:
7,966 HTTP requests
233.18 requests/second
However, 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 second
All four CRUD operations failed this threshold at 500 VUs.
The overall HTTP results were:
Average: 1.51 seconds
Median: 1.16 seconds
P90: 3.21 seconds
P95: 3.31 seconds
Maximum: 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/second
However, 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
↓
PostgreSQL
while the AWS implementation was:
React
↓
API Gateway
↓
Lambda
↓
RDS PostgreSQL
The 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 capacity
Potential 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 Proxy
Each optimization should be tested independently.
For example:
Configuration A
Lambda → RDS

vs

Configuration B
Lambda → RDS Proxy → RDS
Then 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
└── RDS
This 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 seconds
but the P95 was:
3.31 seconds
This 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:
React
Node.js
Express.js
PostgreSQL
It was then migrated to:
React
API Gateway
AWS Lambda
RDS PostgreSQL
Using 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article