Delay node - rate limit strange behaviour. Confusing limit rates. Is it lying us (a little)?

Hi everyone,

I’m having a question about the Delay node in Node-RED when using the rate limit option. (Node red version 4.0.8)

For example, I configured the node to 10 messages per minute, but the behavior is not what I expected.

Instead of allowing up to 10 messages anywhere within the minute, the node seems to allow one message every 6 seconds, as if it were converting the rate limit into a fixed interval. I don't know exactly if it does maths in seconds, miliseconds, ...

In other words:

  • What I expected: a maximum of 10 messages in any 60-second window
  • What I get: (see and test example below)

The result is not what I expect, since I need to allow short bursts as long as the total per day is not exceeded.

Is this the expected behavior of the Delay node?
Is there a native way (or a recommended alternative) in Node-RED to implement a true sliding-window rate limit, instead of this strange behaviour?

Thanks in advance for any help.

Example:

  • The attached flow is a minimal example to show the behavior I’m seeing.
  • Use two inject nodes.
  • If you send 5 msg at 1 msg each 7 seconds, all messages are correctly sent to 1st output in delay node
  • If you send 5 msg at 1 msg each 5 seconds, same of them are sent to 2nd output despite the rate is less than 10 msg/minute.
  • the 10msg/min node has an strange behaviour, hasn't it?

[
    {
        "id": "5704b41a79567472",
        "type": "tab",
        "label": "Flow 2",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "60c6aa58e443d024",
        "type": "debug",
        "z": "5704b41a79567472",
        "name": "debug",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1170,
        "y": 260,
        "wires": []
    },
    {
        "id": "fc774e569bf24db2",
        "type": "change",
        "z": "5704b41a79567472",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "out",
                "pt": "msg",
                "to": "2nd",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1030,
        "y": 320,
        "wires": [
            [
                "60c6aa58e443d024"
            ]
        ]
    },
    {
        "id": "c67a43335950adbe",
        "type": "delay",
        "z": "5704b41a79567472",
        "name": "10/min",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "10",
        "nbRateUnits": "1",
        "rateUnits": "minute",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": true,
        "allowrate": false,
        "outputs": 2,
        "x": 810,
        "y": 320,
        "wires": [
            [
                "60c6aa58e443d024"
            ],
            [
                "fc774e569bf24db2"
            ]
        ]
    },
    {
        "id": "f9d520e7e8f322f3",
        "type": "inject",
        "z": "5704b41a79567472",
        "name": "",
        "props": [
            {
                "p": "timestamp",
                "v": "",
                "vt": "date"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 270,
        "y": 320,
        "wires": [
            [
                "455b795e61c69190"
            ]
        ]
    },
    {
        "id": "8eca9fe9dfbcd446",
        "type": "delay",
        "z": "5704b41a79567472",
        "name": "",
        "pauseType": "rate",
        "timeout": "7",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "7",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 640,
        "y": 320,
        "wires": [
            [
                "c67a43335950adbe"
            ]
        ]
    },
    {
        "id": "455b795e61c69190",
        "type": "function",
        "z": "5704b41a79567472",
        "name": "send 5 msg",
        "func": "const messages = [];\nfor(let i = 0; i < 5; i++){\n    messages.push({\n        nr: i+1\n    });\n}\nreturn [messages];",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 430,
        "y": 320,
        "wires": [
            [
                "8eca9fe9dfbcd446"
            ]
        ]
    },
    {
        "id": "99f7af637ff2c8f1",
        "type": "inject",
        "z": "5704b41a79567472",
        "name": "",
        "props": [
            {
                "p": "timestamp",
                "v": "",
                "vt": "date"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 270,
        "y": 400,
        "wires": [
            [
                "7ca23bbaf6e198c4"
            ]
        ]
    },
    {
        "id": "2db82a9f2d0c4fbd",
        "type": "delay",
        "z": "5704b41a79567472",
        "name": "",
        "pauseType": "rate",
        "timeout": "7",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "5",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 640,
        "y": 400,
        "wires": [
            [
                "c67a43335950adbe"
            ]
        ]
    },
    {
        "id": "7ca23bbaf6e198c4",
        "type": "function",
        "z": "5704b41a79567472",
        "name": "send 5 msg",
        "func": "const messages = [];\nfor(let i = 0; i < 5; i++){\n    messages.push({\n        nr: i+1\n    });\n}\nreturn [messages];",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 430,
        "y": 400,
        "wires": [
            [
                "2db82a9f2d0c4fbd"
            ]
        ]
    }
]

Related: The Rate Limit node is lying to you (a little)
@waquete
@HaroldPetersInskipp
Have you found any solution?

You are correct in that it converts everything into a single fixed interval. It uses a simple single timer to decide to block or allow the next message through.

Thanks, I think this behavior is quite confusing given the node’s configuration options and the description in its help text. Even we don't know how it converts: minutes? seconds? miliseconds?

Would it make sense to reconsider or adjust how this works? Unfortunately, I’m not able to implement such a change myself.

You could use a simple gate, open the gate, allow a count of 10 messages to go through, close the gate and then open the gate again after 1 minute (that minute starting when you decide it's reference). You could count the time in milliseconds from the timestamp, when at one minute open gate.

A simple function node is a workaround for now

[{"id":"5395ba2ccbd9c367","type":"function","z":"beff51c13e9e8c28","name":"burst filter","func":"const timeout = 10000;  // 10 seconds in milliseconds\nconst burst = 3;        // number in burst\n\nlet inflight = context.inflight || 0;\n\nif (inflight >= burst) { \n    node.status({ fill: \"red\", shape: \"dot\", text: burst });\n    return; \n}\n\ninflight += 1;\n\nsetTimeout(function() {\n    let v = context.inflight\n    v -= 1;\n    context.inflight = v;\n    node.status({ fill: \"green\", shape: \"ring\", text: v });\n}, timeout)\n\ncontext.inflight = inflight;\nnode.status({ fill: \"green\", shape: \"ring\", text: inflight });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":285,"y":112.5,"wires":[["343936e38ee83572"]]}]

edit the constants at the top as you desire

1 Like

@dceejay Thanks a lot for your proposal. I have found two solutions:

  1. This one: @cameo69/node-red-ratelimit
  2. And yours by adding a 2nd output (in my case it is what I want). I need a daily rate limit that I exactly dont know (api calls) it works. I am not sure if it resets every day at 00:00, or it is like your code (lets say a 24hours timer that reduces inflight counter). But I am going to start your code (it will be easy to change with a first code that checks if we have "changed" the day since de previous call).

It is a pitty not to have this implemented in the delay node itself.

Thanks a lot.

const timeout = 1000*60;  // 60 seconds in milliseconds
const burst = 10;         // number in burst

let inflight = context.inflight || 0;

if (inflight >= burst) { 
    node.status({ fill: "red", shape: "dot", text: burst });
    return [null, msg]; 
}

inflight += 1;

setTimeout(function() {
    let v = context.inflight
    v -= 1;
    context.inflight = v;
    node.status({ fill: "green", shape: "ring", text: v });
}, timeout)

context.inflight = inflight;
node.status({ fill: "green", shape: "ring", text: inflight });

return [msg, null];

but maybe...

3 Likes

Great, thanks a lot @dceejay! I have just marked this as the solution. (If anyone reads this before the PR is merged, please check the previous post for a temporary solution.)

Hi, you can save the hassle and use the rate limit note. It does exactly what you want. It gives you exactly the number of messages you specify in a sliding window.