Quick answer: Set Rigidbody.collisionDetectionMode to Continuous (against static geometry) or ContinuousDynamic (against other moving Rigidbodies). For very high velocities, also clamp speed and make colliders thicker than the distance the object can travel in one physics step.

Your bullet or fast-falling player passes clean through a floor and falls out of the world. In the Editor at normal speed it collides fine, but crank up the velocity and it tunnels straight through. This is one of the most commonly reported physics bugs in Unity games, and it comes down to how discrete collision detection works at the boundary of a thin collider.

The Symptom

The Rigidbody works perfectly at low speeds but passes through colliders above a certain velocity threshold. The threshold depends on the collider’s thickness and the fixed physics timestep. A flat floor plane (nearly zero thickness in the collision geometry) will be tunneled through by almost any object moving faster than a few units per second. A floor with a collider several units thick may never exhibit the problem at walking speed but fails reliably for projectiles or during a high-gravity fall.

You might also observe the problem only on lower-end hardware or at lower frame rates, where Unity’s physics simulation may accumulate larger catch-up steps.

What Causes This

Unity PhysX uses a discrete collision detection strategy by default. Every fixed timestep (default 0.02 seconds — 50 Hz), the engine moves all Rigidbodies to their new positions and then checks for overlaps. If an object moves far enough in that single step to completely skip over a collider — entering from one side and exiting the other without any intermediate position ever overlapping the geometry — the physics engine never registers a collision.

This is called tunneling. The maximum safe speed for a given collider thickness is:

// maximum safe speed = collider_thickness / Time.fixedDeltaTime
//
// Example: floor BoxCollider is 0.2 units thick, fixedDeltaTime = 0.02s
//   0.2 / 0.02 = 10 units per second maximum with Discrete mode
//
// A bullet at 80 units/s travels 1.6 units per step.
// Any collider thinner than 1.6 units is tunneled through.

The Fix

Set the collision detection mode

Unity provides four collision detection modes on Rigidbody:

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Bullet : MonoBehaviour
{
    private Rigidbody rb;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // ContinuousDynamic so this bullet detects collisions
        // with both static floors and moving enemies.
        rb.collisionDetectionMode =
            CollisionDetectionMode.ContinuousDynamic;

        rb.useGravity = false;
    }

    public void Fire(Vector3 direction, float speed)
    {
        rb.linearVelocity = direction.normalized * speed;
    }
}

For the player character (falls fast but is not a projectile), Continuous is usually sufficient and cheaper than ContinuousDynamic:

private void Awake()
{
    rb = GetComponent<Rigidbody>();

    // Prevents falling through floors; the player doesn't need
    // continuous collision against other dynamic bodies.
    rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
}

Clamp maximum velocity

Regardless of detection mode, setting an upper bound on velocity prevents edge cases caused by physics explosions (two colliders overlapping due to a bug, pushing each other at enormous speed) and keeps the simulation stable:

using UnityEngine;

public class VelocityClamp : MonoBehaviour
{
    [SerializeField] private float maxSpeed = 40f;
    private Rigidbody rb;

    private void Awake() => rb = GetComponent<Rigidbody>();

    private void FixedUpdate()
    {
        if (rb.linearVelocity.magnitude > maxSpeed)
            rb.linearVelocity = rb.linearVelocity.normalized * maxSpeed;
    }
}

Make colliders thicker

For static level geometry, the cheapest fix is to make the collider thicker. Use a BoxCollider instead of a flat MeshCollider and extend it downward so it is at least 0.5–1.0 units thick. Offset the center to keep the top surface flush with the visual mesh:

private void OnValidate()
{
    var box = GetComponent<BoxCollider>();
    if (box == null) return;

    // Top surface stays at local Y=0, collider extends 1 unit down.
    box.size   = new Vector3(box.size.x, 1.0f, box.size.z);
    box.center = new Vector3(box.center.x, -0.5f, box.center.z);
}

Configure the layer collision matrix

Continuous detection has a per-pair cost. Only enable it for layers that actually interact at high speed. In Edit › Project Settings › Physics, open the Layer Collision Matrix and disable collisions between pairs that never need to interact (bullets vs. decorative props, for example). In physics queries, always pass a layer mask to avoid evaluating irrelevant layers:

private void CheckGrounded()
{
    // Only query the Environment layer (layer index 6)
    int environmentMask = 1 << 6;

    bool isGrounded = Physics.Raycast(
        transform.position,
        Vector3.down,
        0.15f,
        environmentMask
    );
}

Physics.queriesHitBackfaces

By default, Physics.Raycast and related queries only detect the front face of a collider. If you need a bullet to detect the exit point of a solid object (for penetration effects or tunneling detection logic), enable backface hits:

private void Awake()
{
    // Enable back-face hit detection globally.
    // Useful for projectile penetration raycasts.
    Physics.queriesHitBackfaces = true;
}

This is a global setting. It increases the number of hits returned by raycasts and has a small performance cost, so consider keeping it scoped to specific queries by toggling it on and off around the calls that need it.

Related Issues

As a rule of thumb: Continuous for players and important items, ContinuousDynamic for bullets, Discrete for everything else — and make your floors at least 0.5 units thick.