Data Store Limit In Saving Voxel Terrain

I want to store a whole voxel map in data store that is 1000 x 1000 x 60 voxels/block. I want to store each individual voxel’s data such as position, name/id, rotation, and other data like inventory (maybe it’s a chest).

I need to know if it’s possible to store this much data in data store before I get started on working on this project. The amount of voxels could be up to 60,000,000+.

I tried looking up things related to this, but I just can’t find anything on this.

By the way the data should look:

  {
        {
            ID = "Chest", -- prefab name
            Position = Vector3.new(120.5, 45.25, -87.75),
            Rotation = Vector3.new(0, 90, 0),
            Data = {
                Inventory = {} -- players could insert items here
            }
        },
        {
            ID = "CrackedPipeSegment", -- prefab name
            Position = Vector3.new(130.0, 46.0, -90.0),
            Rotation = Vector3.new(0, 0, 0),
            Data = {
                IsPowered = true,
                FlowRate = 2.5
            }
        }
    }

I am not sure if I should store the data in chunks or all just one table containing the voxels/blocks?

2 Likes

I think the best idea would be to generate terrain with a seed. Then you can save the seed. If the player will be changing the terrain, you can save what edits were made in what order and apply them that way.

You could also handle chests separately instead of terrain.

I think that would be the best method.

Good luck!

2 Likes

1 question what should I do if the player placed block?

How would I manage structures and trees that spawn? Wouldn’t it have to be managed separately?

Should I store the modified voxels as strings or tables?

I should warn you that Roblox’s DataStore limits in 2026 are going to make voxel games very challenging or potentially unrealistic.

The real issue here is the cap of 100 MB + 1 MB per unique user that has played your experience.

Firstly, you won’t even get the full 4 MB that an individual DataStore would normally get:

Each player basically gets a max of 1 MB worth of data. Yes there’s that initial 100 MB buffer that will cover it for a while, and this won’t hurt 99% of experiences, but it hurts this genre a ton. Each player will increase their data size for a given world as they continue to play, but they may also create multiple worlds where this would happen. Now yes, not every player will actively play and may not use their full 1 MB, but relying on things like that is not how you should go about this sort of thing. You should always plan for the worst-case scenario to be prepared for it.


But let’s say you didn’t have those limitations as mentioned above. Firstly, you’ll want to optimize your data as much as possible. The way you have it now, it’s going to fill up very quickly. I’ll use my own project as an example, that’s very similar to Minecraft. Each chunk is 16 blocks wide and 256 blocks tall. That equals a total of 65,536 blocks. The worst case scenario of a chunk is that every single block is filled and uniquely different than the seed. The most optimal way to save a basic block would be to use 5 bytes per block, aka 5 characters. Using an exact amount of characters per block means that no commas will have to be used, which will actually save a lot of space. That would be a total of 327,680 bytes per chunk, leaving room for about 11 more chunks in a single DataStore key.

So, why 5 bytes per block? Well, to answer that, I’ll first have to explain what a byte is. A byte is 8 bits, each of which could be a 0 or a 1. With 8 bits, that leaves a total of 256 possible combinations between 0’s and 1’s. This is calculated by 2 ^ numBits, so 2 ^ 8 = 256. We can store a lot of data with that. The first two bytes of the block will be the positional data. But instead of storing large values for positional data, (as the further a block is away from the world center, the larger its position data will be), we’ll use an offset instead. This means that the block’s positional data can be 0 to 15 from the chunk’s horizontal position, or 0 to 255 from the chunk’s vertical position.

This means that the height can be one byte. The X and Z axis can actually share a byte by splitting it in half. If a byte is 8 bits, half a byte is 4 bits. If you do the math again, you’ll find that half a byte gets you 16 combinations (2 ^ 4 = 16). Which means the X and Z axis both get half a byte each, and that gets combined into a single byte again. The third byte will also be halved, one part for the light level of 0 to 15, and the other half for the block direction (also this is a bit extra as there are only 6 directions a block could face, but this could also be used for block states, and you could use different amounts of bits depending on what you actually want to store). The final two bytes are for the block ID, and 2 bytes gives 16 bits, which means we get 65,536 possible block IDs (2 ^ 16 = 65,536).

All of this means that you could get a total of 12 chunks per DataStore key. However, this doesn’t count for entities, mobs, or even special block types like storage blocks. Those all have their own methods of optimization as well. You actually have 262,144 extra bytes leftover in the DataStore on top of the 12 chunks, so you can use that to store the rest. But you’ll have to impose some sort of limitation so that the max size can’t go over. However, this could be optimized much further. Obviously, you’ll be using a seed so that the only thing you need to save are changes made to the world. But if we do that, then we don’t need to fill up a whole chunk’s worth of data for a single block change. So you could use a dynamic optimization system to dynamically group chunk data together so that you could cover far more chunks at once, highly increasing the number of edited chunks that you could load at once time.

I nearly forgot to clarify that these bytes are done through characters via string.char and string.byte. You will need to handle data larger than 255 (as char accepts numbers 0 to 255) in your own way though.

But do keep in mind of the 2026 DataStore cap limit as it kind of kills this sort of project. Because then you’d only be able to store 3 chunks + 65,536 bytes left over (at least before the dynamic grouping optimization). I really hope Roblox decides to go back on this update as it literally kills this sort of project unless you’re okay with severe limitations. I have ideas on how this could still work, but it’d definitely be a worse experience in comparison.


Now I’ll answer your other questions.

I’ve basically answered this already, but basically, you only need to save chunks that have altered data. Everything else will be generated from the seed. So when the chunk loads from the seed, use the chunk’s modified data to overwrite the previous default data.

All of this is done with Perlin Noise via math.noise. Noise can be used in many different ways. You use it for terrain, you use it for caves, you use it for ore, spawn locations, everything. Also yes, this is a multi-stage process, even Minecraft does it in stages.

I also answered this already, but I’ll explain more clearly. You’ll basically be doing a bit of both. The raw block data would be a single long string containing every modified block with the 5 characters I mentioned. But you’ll use a table structure to identify individual chunks, as well as more specific chunk data, such as pointers to storage containers.

2 Likes

Hey how should the block data look? I am struggling to wrap my head around characters being used in correlation to a byte, and how splitting a byte works?

Every normal character will correlate to a byte. Just look at ASCII for an example.

It goes all the way up to 255 (although I think there are some things beyond that, but that’s not what we’re looking at for this case). That means you can convert any number between 0 and 255 into a single string character. The part you’ll have to do is make a system to convert binary into a number and back, then you can use that number to turn into a character and back. So if you had a block at X:5, Z:10, it might look like 0101 1010 because 0101 = 5 and 1010 = 10. Then, 01011010 = 90, which you can then use string.char(90) and get Z. Which means, if you use string.byte('Z'), you’d get 90, and then you can convert 90 back into 01011010, which you know is split in half, so 0101 1010, which is 5 10. Make sense?

1 Like

Wow makes much more sense! So string.char() function converts a number into a binary number?

string.char() will convert a number between 0 and 255 into a single string character. string.byte() will convert a single string character into a number of 0 to 255. Neither of these are binary yet; that’s something that you’ll have to code yourself. There are probably some modules out there somewhere to simplify the process for you, if you don’t want to write it yourself. The binary is only needed if you’re trying to split a byte up.

1 Like

For these kinds of minecraft games, you should probably not store the whole generated world. If the world is generated by a seed, you can just store that + and changes made to the map. That reduces the save size by a lot

1 Like

Yes, but you still have to plan for the worst case scenario of every block being changed. You could get around that by saving using a grouping technique, that way placing one block doesn’t reserve an entire chunk. However, that also introduces additional overhead and complexity. There might be a case where one chunk already has data in one DataStore, but you come back to it much later to modify it again. In that case, there’s a chance that the DataStore it used previously is now full, which means that this one chunk will now require two different DataStores to load all of the changes to it. This could be resolved by dynamically shifting where data is stored, but that’s even more overhead and complexity, which is made far worse by the limitations of Get/SetAsync on Roblox.

This is why I stick to reserving whole chunks, so that I know that this chunk will always correspond with one grouping of a DataStore. Maybe there’s a much smarter way to do everything I mentioned above, but for the time being, this makes the most sense to me right now.


Although, one other method I just thought of would be only saving blocks that a player has modified, and saving it to that specific player. You could probably have one DataStore keep a map tracking what players have modified what chunks, that way the server can know which player DataStores need to be loaded to fully load that given chunk. This would give players a total of 209,715 block modifications each. However, you might want to reduce that to store other data like the player’s inventory and stats, along with storage containers taking up much more space.

The only issue with that is this would include mining, so players could reach this limit quite a bit faster than one might realize. That’s 91 player inventory’s of full stacks. Which sounds like a lot, but measure that over the course of a world (not to mention that this limit is for the entire experience, not just per world). Maybe you could remove air blocks from areas that haven’t been visited in a long time, that would certainly help a lot, but that would mean regenerating the underground. Players might not want that, so maybe there can be a setting for it. I mean, there’s a number of hacky ways that you could perhaps make this work still, but I don’t think there will ever be a perfect solution anymore.

3 Likes

This is a very challenging thing to do with DataStore being super limited. I am not sure if this something I can proceed with. Also, if the player is able to place blocks in a world that has been modified a lot, this can result in a limited amount of blocks the player can place. The fact that the block amount is limited and prevents the player from adding more blocks to the world makes this game almost impossible.

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.