Like everything else in Ethereum, limits are imposed by the gas consumed for your transaction. While there is no exact size limit there is a block gas limit and the amount of gas you provide has to be within that limit.
When you deploy a contract, there is an intrinsic gas cost, the cost of the constructor execution, and the cost of storing the bytecode. The intrinsic gas cost is static, but the other two are not. The more gas consumed in your constructor, the less is available for storage. Usually, there is not a lot of logic within a constructor and the vast majority of gas consumed will be based on the size of the contract. I'm just adding that point here to illustrate that it is not an exact contract size limit.
Easily, the bulk of the gas consumption comes from storing your contract bytecode on the blockchain. The Ethereum Yellowpaper (see page 9) dictates that the cost for storing the contract is
cost = Gcodedeposit * o
where o
is the size (in bytes) of the optimized contract bytecode and Gcodedeposit
is 200 gas/byte.
If your bytecode is 23kB, then your cost will be ~4.6M gas. Add that to the intrinsic gas costs and the cost of the constructor execution, you are probably getting close the block gas size limits.
To avoid this problem, you need to break down your contracts into libraries/split contracts, remove duplicate logic, remove non-critical functions, etc.
For some more low level examples of deployment cost, see this answer and review this useful article on Hackernoon.