I often see this pop up in google results, which I've had to do quite a bit of in order to get what I wanted to work, so this is me paying it forward.

The rundown
Data stores can save 262,143 bytes. 2^16-1, however you can't write a byte. Even numbers aren't saved in efficient byte format, they're converted to characters, and each character is considered (at least) one byte.

Where it gets bad
You're offered a limited about of requests per minute, so naturally the thing to do is to group things that you can save together, the only way to do that is tables. However everything is parsed through a JSON encoder, meaning a 3x3 table of 2 digit numbers gets you 100 characters. For most people, this will never be a problem, unless you're doing something crazy.

Where it gets ugly
If you try to save a procedual map, or want to save something between games, once you get into these 'big data' type objects you'll hit the cap super quickly. An array of 1024x1024 with each point representing a height is ~1 million points on it's own, and remember in the example above 9 points became 100 characters. ~10 million characters would take ~40 seperate saves by a datastore in order to carry over it you split it up, and if you have a multi layered map, or a seperate table for what type of tile each point represents, each layer adds another 40 data stores required.

How to optimize
string.char(n) gives you a character representing a byte with the value of n, where n is between 0 and 255. Unfortunately, you can't use half of those. UTF-8 is the format JSON requires, so 128-255 is out since the first bit is set in that range. Additionally several characters get padded to 5 times their original length. Things like / and %, which traditionally are used to mark 'yes I did mean that character'. Culling those leaves us with 94 total characters, those are:

32, 33, 35-91, 93-127.

How does that help us? We can't use the binary system since it's not a power of 2, nor a power of anything else. The closest you can get into splitting it is into two base nine values, however that leaves us with some weight, but useful if you need to save a bunch of small values within a range of 0-8, since you can store two per character. The most likely use is simply a base 94 system, which gives a maximum value of 93, 8835, ~830k, depending on how many characters we user per value, as explained below

Turning a integer into a character
First decide on a 'byte' size, if none of your values exceed 93, use one, otherwise sum the values of 93*94^n, starting from 0 until you hit the maximum value you'll be using, and use that n value size.

Start with an empty string. Loop backwards, starting from the value you picked until 1. If the value you're encoding is greater than 94^(iteration-1) subtract that much from from the value and increment a counter. Loop this check 93 times or until the condition is false. Take this value, and take it as the value of the first 'bit' in your 'byte' Make a table of all the permitted string.char candidates (32, 33, 35-91, 93-127) and select from the table the entry at that particular count. Append a character using that value with string.char to your finalized string.

At the end you should have a string of the length of your byte size.

Turning a byte of characters into an integer
This process is easier, use string.byte(string, n, n), find the position of that value on your table of candidates, and multiply by 94 ^ (ByteSize - n). Loop for each 'bit' in your byte, incrementing n as you do.

How do I encode values in tables?

Once you're out of the range of large numbers, you can encode metadata tables to help interpret the data you saved, they can include things like the datastores you have the information split over, the byte size used, the size of different sets of data if you combined them together, so you know where to split a string to pull another set of data out.

Something like the "MapMetadata" data store might have a table like:
{ map = "MapSave9223, --Datastore to load for the map
MapByteSize = 2, MapXSize = 255, MapYSize = 255, --You know from this the map has 65,536 points, each taking two characters, and each row ends after 512 characters, or 255 points.
BuildingDataOffset = 131073, --What character another set of data gets saved on
SaveVersion = 5 --Important to put a version into metadata, if you end up adding something and need to encode or load data different you can be backwards compatible if you know what version something was saved on last

If you have any questions you can send them to [email protected] I don't know how often I'll be checking on this, but you can always leave a reply and I'll answer it for posterity eventually