Introduction
Forms are one of those parts of a web application that look simple until real users start submitting them at the same time.
A registration form, checkout form, support request, or profile editor may receive dozens, hundreds, or thousands of requests while the application is running. The validation logic needs to remain correct under that load, while the server must continue generating responses without unnecessary work.
ASP.NET Core static server-side rendering provides an interesting model for this scenario. The server receives the request, processes the form, validates the submitted data, and returns HTML.
This article looks at form validation in a static SSR application and focuses on a practical question: what happens when multiple users submit forms concurrently?
The goal is not to claim a particular throughput number. Performance depends heavily on the application, hardware, database, network, and validation rules. Instead, we will build a reproducible testing approach and identify the areas worth measuring.
Understanding Static SSR Form Submission
With static SSR, the browser initially receives HTML generated by the server.
A simplified form flow looks like this:
Browser
|
| GET /register
v
ASP.NET Core
|
| Render form
v
HTML response
|
v
Browser
|
| POST form
v
ASP.NET Core
|
| Validate
v
Success / Validation response
The server remains responsible for processing the submitted form.
This makes the server-side validation path especially important.
A well-designed application should not depend only on browser-side validation because client-side validation can be bypassed.
Creating a Simple Static SSR Form
Consider a registration model:
using System.ComponentModel.DataAnnotations;
public class RegistrationModel
{
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[MinLength(8)]
public string Password { get; set; } = string.Empty;
}
A Razor component can expose the form:
@page "/register"
<EditForm Model="model" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>Name</label>
<InputText @bind-Value="model.Name" />
<ValidationMessage For="@(() => model.Name)" />
</div>
<div>
<label>Email</label>
<InputText @bind-Value="model.Email" />
<ValidationMessage For="@(() => model.Email)" />
</div>
<div>
<label>Password</label>
<InputText type="password"
@bind-Value="model.Password" />
<ValidationMessage For="@(() => model.Password)" />
</div>
<button type="submit">Create Account</button>
</EditForm>
@code {
private RegistrationModel model = new();
private void HandleSubmit()
{
// Process valid form submission.
}
}
The exact form configuration depends on the Blazor rendering mode and application architecture, but the principle is straightforward: validate the submitted model on the server before performing the business operation.
Why Server-Side Validation Matters
Consider a registration endpoint that performs this sequence:
Receive request
|
v
Validate input
|
v
Check business rules
|
v
Check database
|
v
Create account
If validation happens after expensive database operations, invalid requests can consume unnecessary resources.
A better approach is to reject obviously invalid input as early as possible.
For example:
if (string.IsNullOrWhiteSpace(model.Email))
{
return;
}
if (!new EmailAddressAttribute().IsValid(model.Email))
{
return;
}
In a real application, use the validation system consistently rather than duplicating validation rules throughout handlers.
Validation and Business Rules Are Different
Data annotations are useful for basic input validation.
For example:
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
But business validation can be more complicated.
A registration process might require:
Email uniqueness
Account eligibility
Password policy
Organization membership
Invitation validation
Those checks usually require application or database access.
A useful validation pipeline is:
Input Validation
|
v
Business Validation
|
v
Database Validation
|
v
Business Operation
Keeping these stages separate makes the code easier to test and helps prevent unnecessary database calls.
Handling Concurrent Requests
Suppose 100 users submit the form at approximately the same time.
The server may process requests concurrently:
Request 1 ----\
Request 2 -----\
Request 3 ------> ASP.NET Core
Request 4 -----/
Request 5 ----/
The application should not store request-specific information in shared mutable state.
For example, this is dangerous:
public static RegistrationModel CurrentRegistration { get; set; }
Multiple requests can overwrite the same object.
Instead, keep request data local to the request:
public async Task ProcessRegistration(
RegistrationModel model)
{
// Work with this request's model.
}
This allows independent requests to be processed safely.
Avoiding Shared Mutable State
A common mistake in server-side applications is using a singleton service to hold data that belongs to an individual request.
For example:
builder.Services.AddSingleton<RegistrationState>();
If RegistrationState contains the current user's form data, concurrent requests can interfere with each other.
A better lifetime depends on what the service actually represents.
For request-specific work:
builder.Services.AddScoped<
RegistrationService>();
The important rule is not simply "always use scoped."
It is:
Choose a service lifetime that matches the lifetime of the data it owns.
Testing Concurrent Form Submissions
A load-testing tool can generate concurrent HTTP requests.
For example, a simple HttpClient test can issue multiple requests:
var tasks = Enumerable.Range(0, 100)
.Select(async index =>
{
var content = new FormUrlEncodedContent(
[
new("Name", $"User {index}"),
new("Email", $"user{index}@example.com"),
new("Password", "Password123")
]);
return await client.PostAsync(
"/register",
content);
});
var responses = await Task.WhenAll(tasks);
This is useful for a basic concurrency test, but it is not a replacement for a dedicated load-testing tool.
For serious performance testing, tools such as k6, JMeter, or another HTTP load-testing platform provide better control over concurrency, duration, ramp-up, and reporting.
Designing a Useful Load Test
Avoid immediately sending thousands of requests to an application.
Start with a small test and increase concurrency gradually.
For example:
10 concurrent requests
|
v
25 concurrent requests
|
v
50 concurrent requests
|
v
100 concurrent requests
|
v
Higher load if required
At each level, observe:
Response time
Error rate
CPU usage
Memory usage
Database activity
Request throughput
This helps identify where the application begins to struggle.
Testing Valid and Invalid Requests
A realistic test should not send only successful forms.
Include different request categories.

Join the conversation! Your thoughts help the community grow.