When you invoke the Serial.readUntil method, it will create an instance of the String class and append single characters to the buffer, until the timeout is reached.
To append a character, the String class first checks if its internal buffer is large enough to hold the new data plus the old data. Because the internal buffer starts out empty, most of the times you append to a String it will need to dynamically allocate more memory for its internal buffer.
Imagine you have 100 bytes of memory. First, you allocate 20 bytes for a serial command to be received. Next you reserve 50 bytes to unpack the serial command into separate payload sections. You have now allocated 70 of 100 bytes, leaving you with 30 bytes of free memory.
-------------------------------------
| 20 bytes | 50 bytes | 30 bytes |
| used | used | free |
-------------------------------------
Now your receiving loop ends and the memory allocated for the serial command is free'd. You have 30 bytes + 20 bytes = 50 bytes of free memory.
-------------------------------------
| 20 bytes | 50 bytes | 30 bytes |
| free | used | free |
-------------------------------------
Let's say now you need another 40 bytes to create a response String to send over the serial. Although you have 50 bytes of memory left, you will not be able to allocate the 40 bytes you need because they are composed of smaller pieces of free memory.
This is called memory fragmentation. It happens a lot with excessive use of the String class and leads to undefined behaviour (what happens when you can't reserve enough memory to store the incomming command?).