This is a big title which already brought a lot of discussions over decades. I'm not going to go through everything but few tips which discouraged lots of people from using it.

#1. Most of people use cursor in function or sorts of stored procedure wirtten in PL/pgSQL (LANGUAGE plpgsql). In any case, they execute queries like  prepared statements  , so you should be aware that the cursor slowness could be from the function or stored procedure, other than cursor itself. The same thing happens for regular SQL.
See my another link for more detail about it.
SQL slower in postgresql function

#2. use unbound cursor if you have input parameters and parameters are more likely distributed widely over a table/index.
The reason is similar, is to make a new query plan for each SQL according to its input, it adds a bit overhead, but worse to do it for long run.

#3. Be careful with hold option for cursor, I don't know why it can't be done in application level.

#4. Close cursor as soon as possible, especially if you use cursor for update.


Nothing better than example, here is one.

CREATE OR REPLACE FUNCTION f_insertacl() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
    msk INTEGER;
    flag INTEGER;
    rstype INTEGER;
    id character(36);
    parentid character(36);
    r_t_acl t_acl%ROWTYPE;
    t_acl_curs refcursor;

BEGIN
    SET enable_seqscan TO false;
    IF (TG_OP = 'INSERT') THEN
        msk := 0;
        EXECUTE 'SELECT itype FROM t_inodes WHERE ipnfsid = ' || quote_literal(NEW.ipnfsid)  INTO rstype;

        IF rstype = 32768  THEN
            id := NEW.ipnfsid;
            parentid := NEW.iparent;
            rstype := 1;    -- inserted object is a file
            flag := 1;      -- check flags for 'f' bit
            msk := 11;      -- mask contains 'o','d' and 'f' bits

        ELSIF (rstype = 16384 AND NEW.iname = '..') THEN
            id := NEW.iparent;
            parentid := NEW.ipnfsid;
            rstype := 0;    -- inserted object is a directory
            flag := 3;      -- check flags for 'd' and 'f' bits
            msk := 8;       -- mask contains 'o' bit
        END IF;

        IF msk > 0 THEN
            ALTER SEQUENCE serial START 0;

            open t_acl_curs for execute 'select * from t_acl where rs_id=' || quote_literal(parentid) || ' order by ace_order' ;
            FETCH NEXT from t_acl_curs INTO r_t_acl;
            WHILE (FOUND) LOOP
              r_t_acl.rs_id :=id;
              r_t_acl.rs_type :=rstype;
              IF (r_t_acl.flags & flag ) > 0 THEN
               r_t_acl.flags := (r_t_acl.flags | msk) # msk;
                r_t_acl.ace_order :=nextval('serial');
                INSERT into t_acl values(r_t_acl.rs_id,r_t_acl.rs_type,r_t_acl.type,r_t_acl.flags,r_t_acl.access_msk,r_t_acl.who,r_t_acl.who_id,r_t_acl.address_msk,r_t_acl.ace_order);
              END IF;
              FETCH NEXT from t_acl_curs INTO r_t_acl;
            END LOOP;
        END IF;
    END IF;
    RETURN NULL;
END;
$$;


You may realized that it's a similar one I used in
SQL slower in postgresql function

Yes, it does the same function as the old one in the article, but using cursor, which is much faster.

Make a comment or drop me an e-mail if you have further question.