There's no right answer to this, and don't believe anyone who tells you otherwise. That being said, 6 is the perfect number. In fact, if you don't have 6, add a few parameters to get to the perfect number. They don't even have to do anything.

public bool Profit(string step1, string step2, string extra,
                   string nada, bool whyNot, int? iCant)
{
    // don't EVER return false for this.. that's.. just. no.
    return true;
}

Seriously though, with total confidence I can tell you it absolutely.. depends.

Based on personal experience, anything beyond a half-dozen or so usually starts giving off a bad odor. It's a mixture of fish and wet dog hair. I've seen functions with 20 parameters. No one wakes up in the morning with a goal to make a function that looks like that, but over months and years code can grow out of control.

Someone gets a request, and finds that one function that could complete it - just need to pass one more parameter. This happens a few more times. Eventually you have something no one wants to touch with a 10-foot pole. They just throw more code at it and RUN, hoping to get in and out before they get their shoes messy.

forrest-gump-shit-happens

My guess is that if you're asking this question, you've probably already got a function in mind - and something about it is bothering you.

  • Are the parameters wildly unrelated? Maybe it's doing too much and needs to be split up.
  • Are the parameters all similar/related? Maybe there's a way to group them together.

It's easy to end up with out of control functions. Say you start with something pretty basic, like a function that displays info about a user. In the beginning it only shows a person's age.

public void DisplayInfo(string firstName, string lastName, int age)
{
    Console.WriteLine($"{firstName} {lastName} is {age} years old.");
}

But then time passes, requirements come through to hide the age, include a birth date, show job title, etc, etc. Every requirement is just one more parameter, no biggie... but slowly the function grows.

public void DisplayInfo(string firstName, string lastName, int age, DateTime birthDate,
                        bool showAge, string jobTitle, bool isEmployed)
{
    var position = isEmployed ? jobTitle : "unemployed";

    if (showAge)
        Console.WriteLine($"{firstName} {lastName} ({position}), is {age} years old," +
                          $" and their birthdate is {birthDate.ToShortDateString()}.");
    else
        Console.WriteLine($"{firstName} {lastName} ({position}) has a birthdate " +
                          $"of {birthDate.Month}/{birthDate.Day}.");
}

At this point, something isn't sitting right with you. It's time to take a step back and see if it can be cleaned up.

Group similar parameters according to what they represent

The above example is pretty straight-forward. (I mean, it should be. I wrote it like 5 minutes ago for this post. Real code would be far scarier.) Most of the fields are attributes of a person. So we might create a Person class and pass that to the function instead.

public void DisplayInfo(Person person, bool showAge)
{
    var position = person.IsEmployed ? person.JobTitle : "unemployed";

    if (showAge)
        Console.WriteLine($"{person.FullName} ({position}), is {person.Age} years old," +
                          $" and their birthdate is {person.Birthdate.ToShortDateString()}.");
    else
        Console.WriteLine($"{person.FullName} ({position}) has a birthdate " +
                          $"of {person.Birthdate.Month}/{person.Birthdate.Day}.");
}

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public DateTime Birthdate { get; set; }
    public string JobTitle { get; set; }

    public string FullName 
    {
        get { return $"{FirstName} {LastName}"; }
    }

    public bool IsEmployed
    {
        get { return String.IsNullOrEmpty(JobTitle); }
    }
}

Now any other function that accepted a bunch of attributes of a person can all make use of the new class. And it scales better too. Let's say someone ends up adding email address and id fields to the person class. We can add as many fields as we need, and these functions will not have to change in the slightest. Even so, the fields are there if anyone wants to take advantage of them in the future.

public void DisplayInfo(Person person, bool showAge)
{
    Console.WriteLine($"{person.FullName} is {person.Age} years old.");
}
public void EmailUser(Person person, string message)
{
    // email person.FirstName using person.EmailAddress
}
public void ResetPassword(int id, Person person)
{
    // reset password
    EmailUser(person, "Your password's reset!");
}

The above example is C#, but there are similar structures in nearly any language. Python has classes, Go has structs, Erlang has records. There's always a way to group similar parameters together into something that defines what those parameters represent - in the above case, a person.

If they're mostly unrelated, break the function apart

If you can't group the parameters together into some logical unit that represents something, then it's probably time to break it apart.

Let's muck with the original function again, to show a bunch of somewhat disparate fields. There's no easy way to combine these into something meaningful, as they don't really represent anything... unless "a hodgepodge of data to do a half-dozen things" counts. (It doesn't.)

public void ProcessRegistration(int id, string fullName, string levelOfAccess, string emailAddress, bool runReport)
{
    // register the user with ID id, with levelOfAccess access, and get the result
    // store the result of the registration in the database

    var message = new MailMessage("admin@wherever.com", emailAddress, "Registration Successful", "Thanks for registering!");
    // send message using MailKit or another suitable library

    if (runReport)
    {
        // run some report for the registered user that's sent to who knows who
    }

    Console.WriteLine($"Registration for {fullName} succeeded.");
}

Failing to pull the parameters out into a single entity, you should probably look for functionality that can be pulled out into separate functions and do that instead. Perhaps the code above could be split into functions like these:

public string RegisterUser(int id, string levelOfAccess)
{
    // register the user with ID id, with levelOfAccess access, and get the result
    // return the result of the registration in the database
}

public void SendConfirmationEmail(string emailAddress, bool success)
{
    var message = new MailMessage("admin@wherever.com", emailAddress,
                                  "Registration " + (success ? "Successful" : "Failed"),
                                  "Thanks for registering!");
    if (!success)
        message.Body += " Unfortunately, it failed. Please contact support.";
    
    // send message using MailKit or another suitable library
}

public void RunReport(int id)
{
    // run some report for the registered user that's sent to who knows who
}

public void DisplayConfirmation(string fullName, bool success)
{
    var confirmationResult = success ? "succeeded" : "failed";

    Console.WriteLine($"Registration for {fullName} {confirmationResult}.");
}

The number of parameters sent to each function drops, and (bonus!) notice how the names of the functions themselves also sorta document what the function does. It's also going to be far easier to add automated tests for each of these functions, which probably didn't exist for the original gargantuan one that did several unrelated things.