Very slow simple JOIN query

This may or may not work - I'm basing this off a gut feeling that it's joining your tables before the group and filter. I suggest trying the following: filter and group using a CTE before attempting the join:

with
    __posts as(
        select
            user_id,
            count(1) as num_posts
        from
            posts
        group by
            user_id
        order by
            num_posts desc
        limit 100
    )
select
    users.username,
    __posts.num_posts
from
    users
    inner join __posts on(
        __posts.user_id = users.id
    )
order by
    num_posts desc

The query planner sometimes just needs a little guidance. This solution works well here, but CTEs can potentially be terrible in some circumstances. CTEs are stored exclusively in memory. As a result of this, large data returns can exceed Postgres' allocated memory and start swapping (paging in MS). CTEs also cannot be indexed, so a sufficiently large query could still cause significant slow down when querying your CTE.

The best advice you can really take away is to try it multiple ways and check your query plans.