Hooking into ‘authenticate’ causes login to submit on page load

I am trying to understand why it runs even if the form is not submitted on wp-login page load.

That’s just how it works.

If the current action is login which is the default action, then wp_signon() is always called on page load regardless the login form was submitted or not, and thus your function runs because wp_signon() calls wp_authenticate() which fires the authenticate hook.

So yes, this is an expected behavior:

Any time, the login page is loaded, this message is always displayed
even though nothing is submitted

But it can be prevented.

First, set your callback priority to 20, and secondly, check if the $user (the first parameter) is already a WP_Error instance and if so, then simply return the instance. E.g.

add_filter( 'authenticate', 'custom_login', 20, 3 ); // set priority to 20 or more
function custom_login( $user, $username, $password ) {
    // The user might already be authenticated, so return the user object.
    if ( $user instanceof WP_User ) {
        return $user;
    }

    // There's already an error authenticating the user, or maybe the user
    // has just landed on the login page, so just return the error object.
    if ( is_wp_error( $user ) ) {
        return $user;
    }

    // ... your validation/code here.

    return $user;
}

And I thought it might be worth quoting this from the filter documentation:

The
wp_authenticate_user
filter can also be used if you want to perform any additional
validation after WordPress’s basic validation, but before a user is
logged in.

The default authenticate filters in /wp-includes/default-filters.php

add_filter( 'authenticate', 'wp_authenticate_username_password',  20, 3 );
add_filter( 'authenticate', 'wp_authenticate_email_password',     20, 3 );
add_filter( 'authenticate', 'wp_authenticate_spam_check',         99    );

See wp_authenticate_username_password(), wp_authenticate_email_password() and wp_authenticate_spam_check() for details about what the functions do.