How do I have now a duplicated user entry if this is not allowed (and I cannot replicate it)?

Yes, WordPress checks for duplicate emails internally, but not duplicate usernames

To test this I ran this several times via wp shell:

wp_create_user( 'test', 'password', '[email protected]' );

The result on the second attempt was:

=> class WP_Error#1962 (2) {
  public $errors =>
  array(1) {
    'existing_user_email' =>
    array(1) {
      [0] =>
      string(42) "Sorry, that email address is already used!"
    }
  }
  public $error_data =>
  array(0) {
  }
}

When I tried to register a different email with the same username:

wp> wp_create_user( 'test', 'password', '[email protected]' );
=> class WP_Error#1981 (2) {
  public $errors =>
  array(1) {
    'existing_user_login' =>
    array(1) {
      [0] =>
      string(36) "Sorry, that username already exists!"
    }
  }
  public $error_data =>
  array(0) {
  }
}

I also checked the code and found this in wp_insert_user:

https://github.com/WordPress/WordPress/blob/f93ee2ca76164bcae721e4730c92ee1455fa1dd9/wp-includes/user.php#L1645-L1650

    /*
     * If there is no update, just check for `email_exists`. If there is an update,
     * check if current email and new email are the same, or not, and check `email_exists`
     * accordingly.
     */
    if ( ( ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) )
        && ! defined( 'WP_IMPORTING' )
        && email_exists( $user_email )
    ) {
        return new WP_Error( 'existing_user_email', __( 'Sorry, that email address is already used!' ) );
    }

So What’s Causing This?

Right now there’s not enough information to diagnose this, and it doesn’t help that your usernames are emails as it makes it easy to overlook things or treat them as if they’re always the same.

My best guess at the moment is a race condition, which is more likely if you’re using load balancing.

Eitherway, even if it didn’t create multiple users, you still need to handle users double clicking or triple clicking on the signup button else they’ll get an error that the email already exists ( because they’re getting the result of the second request, and the email got used on the first )

A More Reliable Way to Check The Users

Looking at your code:

        $user_id = username_exists($email);
        if (!$user_id && email_exists($email) == false) {
            $user_id = wp_create_user($email, $password, $email);
        } else if ($user_id) {
            ... 'ERROR in Registration, user email already exists ' ...
            return $error;
        }

wp_create_user already does these checks, and returns either a user id, or an error object. In this situation:

  • You’ve doubled the number of checks
  • If user creation fails, your code never discovers this as there is no check on the result

So, lets simplify it:

$user_id = wp_create_user( $email, $password, $email );
if ( is_wp_error( $user_id ) ) {
    // it failed!
    ....
    return $error;
}

Some Other Things to keep in mind:

  • Don’t use emails as usernames, auto-generate them and use a filter to display the email when the username is shown
  • Sanitize!!! Because you only showed a constrained snippet, it’s possible that sanitising or the lack of it is involved in this, but as you won’t share the surrounding code it’s impossible to tell. It could be that your email had trailing spaces and other characters around it
  • You don’t need to use emails as usernames for that to work. WordPress will accept both the username, and the email when logging in, you don’t need to know your username if you’re logging in if you know the email
  • You don’t need to check if the email exists, wp_insert_user already does this internally
  • Your signup form may be more reliable if it ran via a PHP form submission rather than an AJAX request, eitherway you can’t make this atomic but you can try to reduce the number of steps and parallel requests
  • Disable your signup button on the first click for 5 seconds, perhaps swap it for a progress spinner
  • wp_create_user calls wp_insert_user, so use the latter. WP has a lot of “middle men” functions that try to be helpful, but usually they have subtle behaviours for backwards compat reasons that are more annoying than helpful. Cutting out these inbetweens simplifies things and gives fewer steps to debug
  • Put a nonce on your signup endpoint, if you haven’t then you’re wide open to someone creating thousands of users per minute with a quick bash script and Curl