2

I'm struggling to understand how to improve the performance for an HTTP Request that comes with a huge body.

Now, let me explain better what I mean with performance!
Imagine I have a DB with the table "User" that contains 100k elements: if the client wants to visualize all of them, we can:

  1. Send 100k elements all together;
  2. Paginating the requests, so for instance sends 1k elements and 1k more when the user will scroll.

Easy to say, we would rather use the second option: sending 100k elements will have a huge impact. So far so good!

Now, how can thing works the other way around?
Imagine the web-client is able to edit the records he just downloaded and he wants to save the changes on the DB: he will call something like https://mysite/api/update and pass a body with the update elements.
Let's also suppose the client has scrolled all the way down and he downloaded all 100k elements; the first and easiest improvements is to send back to the server ONLY the elements that the web-client actually edited.

Now, imagine the web-client has edited all 100k elements (not very common, but still.. This is more a "I want to know" question).
What are the possibilities here? I can send one HTTP request to https://mysite/api/update with the body containing 100k, but that seems really.. "bad". I mean, it will take forever for the webclient to send it, and the request itself may fail due to timeout.

First thing I thought was: well, just split the single request in multiple ones! So send 100 requests with 1k elements.
This may work, but.. all the requests should modify the database "once", meaning, if the last element update throws an error, NO element should be updated. So, I should perform all the requests and update in the DB as a transaction, but.. We have 100 different request.

The problem with the first "half solution" is that.. If the server is stateful, I might manage to perform the 100 different request in one go, but what if the server is stateless? Each request is completely standalone, so.. How this could work?

The only thing I think it may work is signalR: I open the connection and send elements split in different signalR message, but.. is this a god solution?

Finally, I also thought: well, maybe we can improve this by compressing the HTTP request body! I could use https://www.npmjs.com/package/pako to compress from my JavaScript web-client, but.. I've read that web-client shouldn't really compress the HTTP request body (possible attacks like GZIP bomb).

5
  • "what if the server is stateless" You can't have a stateless server which uses a (persistent) database. Commented Jan 27, 2023 at 9:58
  • I'm sorry, can you explain better? In one of my current architecture, I have a stateless WebApiServer, which then communicate with another Server that exposes methods implementing IRepository, and from there the database is updated. It's done this way because the "other Server" it's used in different places.
    – Jolly
    Commented Jan 27, 2023 at 10:14
  • Your database is state. For example, you could have your stateless server store each partial transaction in a "holding area" in the database, then only move those partial transactions into the main area only when you get the last partial transaction. Exactly how you've implemented that in terms of C#(?) code isn't important. Commented Jan 27, 2023 at 10:18
  • Server that exposes methods implementing IRepository, and from there the database is updated if the commit is Service-to-Service, then you are limited by the remote service interface. Is the remote service under your control? Can you add new features, for example, add new endpoints or even a second API?
    – Laiv
    Commented Jan 27, 2023 at 11:45
  • It might be helpful to say what else is going on while the client is editing those records. Is everybody else prevented from editing anything the client is looking at, in case the “100k” update is going to happen and that edit will be lost? Commented Jan 27, 2023 at 14:40

2 Answers 2

2

For the vast majority of developers, you'll never encounter a situation where the user needs to update 100K records at once from a web client, as noted in Doc Brown's answer.

If you're one of the astronomically unlucky (or just curious about the theory), one way to implement this involves a staging table and some endpoints to handle the transaction. Example:

### DB Schema
Table: Person
- ID : GUID (PK)
- Name : String
- Phone Number : String

Table : Person_Staging
- Person_ID : GUID (PK)
- Batch_ID : GUID (PK)
- Name : String
- Phone_Number : String

Table : Batch
- ID : GUID (PK)
- Expires_After : Timestamp
- Completion_Time : Timestamp

### HTTP Endpoints
POST /api/batch => returns GUID with a new transaction ID
POST /api/batch/{GUID} => adds request data to the staging table
POST /api/batch/{GUID}/commit => moves data from staging table to

A few notes about this implementation.

  • When you actually make the /commit call, you can wrap the staging-to-live changes in a transaction to ensure data integrity.
  • The Expires_After field is useful to prevent unfinished transactions from clogging the database. A scheduled task can delete uncommitted staging changes after this expires.
  • Although it's possible to skip the first step if you want and allow users to define arbitrary transaction IDs, this makes it possible for users to reuse the same ID after a batch is complete, or worse—for two users to commit to the same ID accidentally.
4
  • I'm sorry, I totally lost the notifications and the answer, which seems brilliant! Yes, I'm more curious about the theory. I may be missing one point, so let me know if I've understood correctly: With POST api/batch I query the table Batch, which creates a new GUID. That GUID is used in POST api/batch/{guid}, which is called 100 times, each time with 100 records.. Finally, if ALL the previous 100 api/batch/{guid} calls have ended correctly, I call api/batch/{guid}/commit: if it returns "ok" it means the database have commited all 100.000 records change correctly.. Is this correct?
    – Jolly
    Commented Mar 8, 2023 at 9:22
  • Also, a question: in this case I would need a Staging table for each "normal" table. Imagine that I have Person, Job, Car tables and a lot more: I would need 2x number of table. It would be possible to have ONE staging table? For instance, if that table would save the name of the linked "normal" table and all the data saved as a byte (for instance the JSON used in the API can be transformed into byte). What do you think? Again, for the sake of curiosity :)
    – Jolly
    Commented Mar 8, 2023 at 9:24
  • 1
    @Jolly - regarding your first comment, you've correctly summarized the process I described. Regarding the second, it's up to you whether you use a single staging table with structured data, or use multiple tables that mirror the existing tables. Out-of-the-box, it's probably easier to use a single table, but I'm not a fan of structured data columns. Some persistence frameworks provide tools to make keeping the staging / live data tables consistent. Commented Mar 9, 2023 at 0:11
  • Thank you very much, your answered to a question that bugged me for way too long. I've looked in internet several times here and there (as I said, it's just a theorical question, not real use case right now). Thank you! Could I ask you how you come up with this? Did you just think for my answer, or did you read it in some book?
    – Jolly
    Commented Mar 9, 2023 at 13:40
4

I think it is pretty normal that unrealistic use cases lead to unrealistic issues.

Lets says you have 100k edited records, and different edits were done in some more or less complex or arbitrary manner. Then I think it is unrealistic to expect one wants the changes to be applied in one transaction. If only one of the changes fails, and that rolls all other changes back, way too much information would get lost. Such batches of modifications are usually applied for each record where possible, and where the modification fails, it is logged for which records the change failed, and maybe why. The log information is then kept, send back to the client for further processing, or used elsewhere.

A different case is when those 100K records are edited in a systematic manner, and it is really important that the change must be applied "all-or-nothing". Most of such changes I can imagine won't be initiated by a user-facing web client, they will usually be internal system changes. For example, a change of the internal date/time representation of the "last-logged-in" date of a user could be such a scenario. Such kind of change can and should be executed directly on the server, initiated by an administrator, and, for example, implemented by an UPDATE sql which affects all records at once.

If you really have a regular operation for a web-client which modifies several hundred thousand records, it might be possible to extend the web UI with certain specific "batch" operations. Those operations can be implemented as special server operations which don't suffer from the necessity to transfer all records first to the client and then all back to the server.

Finally, most operations for which it seems to be important that they are done in one transaction can be implemented in a way which mitigates this necessity. Lets take the "last-logged-in-date" example. By adding an extra attribute to the data model which saves the data format in which the date is stored, updating the data format can be applied partially, or in multiple runs in an idempotent manner. Processes which require the "new" data format can check if it is available and react accordingly when not.

1
  • Sorry to you too, I lost the notification of the answer. I know my question is kinda unrealistic, but.. I was starving for this piece of knowledge :) I guess the notification of the failed changes would be a feedback good enough, IF the changes can happen "NOT all-or-nothing". Thank you!
    – Jolly
    Commented Mar 8, 2023 at 9:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.