If I could add onto what @JarodOfOrbiter is saying, bullet collisions only need to be handled on the server when they could hit a player. The pretty effects of flying bullets and bullet holes can all be handled by the clients who shot those bullets and replicated to the other clients.
The server only needs to step in for security when critical game features like health points are at stake (in other words for hit verification). Attacking clients can also actually perform the check to see if the server needs to get involved. If the attacking client wants the server to cause damage then it must ask the server to validate the shot. This shot validation can also tell the server which players it is likely to hit making collision detection even faster on the server by using a very small whitelist. Here is what a simple check may look like on the attacking client / server:
local maxDist = 2
local function isClose(playerPos, bulletRay)
return bulletRay:Distance(playerPos) < maxDist
bulletRay is the unit direction bullet path before effects like bullet drop. Since this check uses C++ to calculate the distance it is likely to perform much better than an implementation in Lua. Each client should be able to perform tens of thousands of these a second without any lag.
I still maintain my position has stated in previous posts: damage and hit verification should only be performed on the server without trusting client information like how long ago the shot was or what their local game state was at the time of the shot. Trusting potential players that may be hit is fine because it is in the attacker’s advantage to tell the server (behave properly) and disadvantage not to tell the server (misbehave).
This method for reducing lag will work regardless of the actual method used to detect hits.