What is the maximum number of blocks you can reach/hit a player in PvP?

Normal

Its three blocks.

Optifine & Fast Math

If you have Optifine installed with Fast Math enabled, your range will slightly vary (but only like 0.0003 more or less).

From Head

Bare in mind the distance is counted from the head, which means that if someone is below you they might be able to hit you (there is head is near your feet) but you may not be able to hit them (your head is up higher).


Clients can only hit an entity if it is "moused over", which is calculated using a ray from the client's eye location. The short answer is that this ray travels for 3 blocks.

The client calculates if the entity is "moused over" with the following method:

//EntityRenderer.java
/**
 * Gets the block or object that is being moused over.
 */
public void getMouseOver(float partialTicks)
{
    Entity entity = this.mc.getRenderViewEntity();

    if (entity != null)
    {
        if (this.mc.world != null)
        {
            this.mc.mcProfiler.startSection("pick");
            this.mc.pointedEntity = null;
            double d0 = (double)this.mc.playerController.getBlockReachDistance();
            this.mc.objectMouseOver = entity.rayTrace(d0, partialTicks);
            Vec3d vec3d = entity.getPositionEyes(partialTicks);
            boolean flag = false;
            int i = 3;
            double d1 = d0;

            if (this.mc.playerController.extendedReach())
            {
                d1 = 6.0D;
                d0 = d1;
            }
            else
            {
                if (d0 > 3.0D)
                {
                    flag = true;
                }
            }

            if (this.mc.objectMouseOver != null)
            {
                d1 = this.mc.objectMouseOver.hitVec.distanceTo(vec3d);
            }

            Vec3d vec3d1 = entity.getLook(1.0F);
            Vec3d vec3d2 = vec3d.addVector(vec3d1.xCoord * d0, vec3d1.yCoord * d0, vec3d1.zCoord * d0);
            this.pointedEntity = null;
            Vec3d vec3d3 = null;
            float f = 1.0F;
            List<Entity> list = this.mc.world.getEntitiesInAABBexcluding(entity, entity.getEntityBoundingBox().addCoord(vec3d1.xCoord * d0, vec3d1.yCoord * d0, vec3d1.zCoord * d0).expand(1.0D, 1.0D, 1.0D), Predicates.and(EntitySelectors.NOT_SPECTATING, new Predicate<Entity>()
            {
                public boolean apply(@Nullable Entity p_apply_1_)
                {
                    return p_apply_1_ != null && p_apply_1_.canBeCollidedWith();
                }
            }));
            double d2 = d1;

            for (int j = 0; j < list.size(); ++j)
            {
                Entity entity1 = list.get(j);
                AxisAlignedBB axisalignedbb = entity1.getEntityBoundingBox().expandXyz((double)entity1.getCollisionBorderSize());
                RayTraceResult raytraceresult = axisalignedbb.calculateIntercept(vec3d, vec3d2);

                if (axisalignedbb.isVecInside(vec3d))
                {
                    if (d2 >= 0.0D)
                    {
                        this.pointedEntity = entity1;
                        vec3d3 = raytraceresult == null ? vec3d : raytraceresult.hitVec;
                        d2 = 0.0D;
                    }
                }
                else if (raytraceresult != null)
                {
                    double d3 = vec3d.distanceTo(raytraceresult.hitVec);

                    if (d3 < d2 || d2 == 0.0D)
                    {
                        if (entity1.getLowestRidingEntity() == entity.getLowestRidingEntity())
                        {
                            if (d2 == 0.0D)
                            {
                                this.pointedEntity = entity1;
                                vec3d3 = raytraceresult.hitVec;
                            }
                        }
                        else
                        {
                            this.pointedEntity = entity1;
                            vec3d3 = raytraceresult.hitVec;
                            d2 = d3;
                        }
                    }
                }
            }

            if (this.pointedEntity != null && flag && vec3d.distanceTo(vec3d3) > 3.0D)
            {
                this.pointedEntity = null;
                this.mc.objectMouseOver = new RayTraceResult(RayTraceResult.Type.MISS, vec3d3, (EnumFacing)null, new BlockPos(vec3d3));
            }

            if (this.pointedEntity != null && (d2 < d1 || this.mc.objectMouseOver == null))
            {
                this.mc.objectMouseOver = new RayTraceResult(this.pointedEntity, vec3d3);

                if (this.pointedEntity instanceof EntityLivingBase || this.pointedEntity instanceof EntityItemFrame)
                {
                    this.mc.pointedEntity = this.pointedEntity;
                }
            }

            this.mc.mcProfiler.endSection();
        }
    }
}


 //Taken from Entity.java
 public RayTraceResult rayTrace(double blockReachDistance, float partialTicks)
{
    Vec3d vec3d = this.getPositionEyes(partialTicks);
    Vec3d vec3d1 = this.getLook(partialTicks);
    Vec3d vec3d2 = vec3d.addVector(vec3d1.xCoord * blockReachDistance, vec3d1.yCoord * blockReachDistance, vec3d1.zCoord * blockReachDistance);
    return this.world.rayTraceBlocks(vec3d, vec3d2, false, false, true);
}

A bit of late response but I do I a few things to add:

I'm been playing PVP games for a while and can confirm that the vanilla reach in survival with perfect ping is 3 blocks. However, in versions 1.8.9 and below it's possible to do something called combo locking where even if both players have 0 ping and perfect aim, you can consistently hit your opponent before they can hit you for in crazy combos. As @Penguin said, This is because if you get the first hit, the upward knockback forces the opponent into a higher y-level, making the distance from your opponent's head and you longer than the distance between your head and the opponent, because the base of a right triangle is shorter than the hypotenuse.

This means the reach of both players is the same, but the distance that it takes to hit each other is unequal. Combo locking is best performed with Speed II and sprint resetting in order to minimize the amount of time that the opponent's reach distance is within 3 blocks. Sweaty players who know these mechanics and use them may seem like they're cheating with reach hacks from their opponent's point of view.

Another factor that impacts reach is ping: If your ping is somewhat on the higher end, you can occasionally hit opponents while they're outside the usual reach distance of 3 blocks. This is because you're sending and receiving delayed packets from the server. The first hit deals knockback to the player, and while the server still hasn't sent you their movement packets, you can hit them again. They're outside reach distance server-side, but client-side they are still within reach. On servers with a heavier anticheat, players with lower ping will benefit instead due to their hit registration being better.

In any case, without plugins, it would be extremely rare to get a hit above 4 blocks. Six block reach is either extremely bad lag or cheats. Usually on a server, players will get 3 block reach on the first hit and a variation of 2-4 blocks during exchanges.

Edit: I just remembered that cheat clients have a maximum reach of 6 blocks without tp aura. So maybe Minecraft has a built-in reach limit of 6 blocks that can't be achieved in vanilla but only with cheat clients, which may explain why some people say server-side is 6 and client-side is 3.