Workflow keeps looping after syncing all records and never exits cycle (no errors shown)

Describe the problem/error/question

Hello dear community,

I’m running into a strange issue with one of my n8n workflows that processes a large database (around 50,000 records). The general logic looks like this:

  1. The workflow starts by fetching the total count of records and setting a counter to zero.

  2. It then enters a loop that fetches data from the database in batches.

  3. After each batch, it checks whether more than one item was returned:

    • If yes → continue the cycle.

    • If no → stop the loop, write a timestamp (marking the sync as finished), and end successfully.

Everything works as expected for most of the process — all 50,000 items are fetched and processed correctly — but when it reaches the last cycle, where only one record should remain and the “stop” branch should trigger, it doesn’t.

Instead:

  • The condition (if > 1) never seems to behave correctly.

  • The workflow just loops again and gets stuck, sometimes indefinitely.

  • There are no visible errors in the execution logs or console, even though the process eventually crashes or times out.

  • The MySQL node often shows as “running” when the whole process stalls.

I’ve tried debugging with manual runs and inspecting the execution data, but can’t find why the condition never evaluates properly or why it doesn’t exit cleanly after the last cycle. And there are no errors in the console.


What I’ve checked:

  • The “If” node logic (looks correct: should trigger failure path when items <= 1).

  • No unhandled errors or exceptions appear in logs.

  • MySQL query executes fine when tested independently.


Question:

Has anyone seen similar behavior where a looping workflow in n8n:

  • Never exits a conditional branch even when the condition should evaluate to false, and

  • Gets stuck in execution without logging any error?

Could it be related to how n8n handles array lengths or item counts in an If node, or possibly something about how MySQL returns the final “one-row” result?

Any insights or debugging ideas would be hugely appreciated :folded_hands:

Please share your workflow

{
  "nodes": [
    {
      "parameters": {
        "operation": "upsert",
        "schema": {
          "__rl": true,
          "value": "public",
          "mode": "list",
          "cachedResultName": "public"
        },
        "table": {
          "__rl": true,
          "value": "ProdReplica",
          "mode": "list",
          "cachedResultName": "ProdReplica"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "student_id": "={{ $('fetchProd').item.json.student_id }}",
            
          },
          "matchingColumns": [
            "uniq_quiz"
          ],
          "schema": [
            {
              "id": "uniq_quiz",
              "displayName": "uniq_quiz",
              "required": true,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        432,
        0
      ],
      "id": "81e4cafb-da34-4158-a45c-bc450923f137",
      "name": "Insert or update rows in a table",
      "credentials": {
        "postgres": {
          "id": "X3LY7hQdwbagmauR",
          "name": "Postgres account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "0a9c85e2-0301-4e4a-9e5f-5f9b1b70efe9",
              "leftValue": "={{ $('fetchProd').all().length }}",
              "rightValue": "=1",
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        832,
        48
      ],
      "id": "22cec9e5-eb40-47ff-9746-d591594012b5",
      "name": "If"
    },
    {
      "parameters": {
        "path": "replicateDb",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -672,
        80
      ],
      "id": "c3dff0e3-a33a-4780-94e8-07b90cf2db53",
      "name": "Webhook",
      "webhookId": "e0b711c7-5564-4217-b353-a73060cd5be2"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE meta\nSET latest_quiz_id = latest_quiz_id + 5000\nWHERE id = 1\nRETURNING latest_quiz_id;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        656,
        0
      ],
      "id": "f6108411-1b4d-4592-b660-619c3718b3a1",
      "name": "set counter ++",
      "executeOnce": true,
      "credentials": {
        "postgres": {
          "id": "X3LY7hQdwbagmauR",
          "name": "Postgres account"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  q.*,\n  s.*,\n  sc.*\nFROM quizes AS q\nLEFT JOIN students AS s\n  ON s.id = q.student_id\nLEFT JOIN schools AS sc\n  ON sc.id = s.school_id\nWHERE q.id > {{ $('getCurrLastQuiz').item.json.latest_quiz_id }}\nORDER BY q.id ASC\nLIMIT 5000",
        "options": {}
      },
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.5,
      "position": [
        0,
        0
      ],
      "id": "8a703b30-2905-4cc1-ac7f-ab8ea785681c",
      "name": "fetchProd",
      "credentials": {
        "mySql": {
          "id": "PJLmOunJDKBtGQeN",
          "name": "MySQL account"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT latest_quiz_id FROM meta WHERE id = 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -224,
        80
      ],
      "id": "a225855e-a406-4a67-8b53-af0e297703d7",
      "name": "getCurrLastQuiz",
      "alwaysOutputData": false,
      "credentials": {
        "postgres": {
          "id": "X3LY7hQdwbagmauR",
          "name": "Postgres account"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE meta\nSET latest_quiz_id = 0\nWHERE id = 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -448,
        80
      ],
      "id": "aa073480-5c07-45a6-b619-fd05b755e90d",
      "name": "reset",
      "credentials": {
        "postgres": {
          "id": "X3LY7hQdwbagmauR",
          "name": "Postgres account"
        }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "meta",
          "mode": "list",
          "cachedResultName": "meta"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": 1,
            "last_update": "={{ new Date().toISOString() }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "latest_quiz_id",
              "displayName": "latest_quiz_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "last_update",
              "displayName": "last_update",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1136,
        64
      ],
      "id": "c2952736-d5d8-4e04-a315-86202005ab0a",
      "name": "set time",
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "id": "X3LY7hQdwbagmauR",
          "name": "Postgres account"
        }
      }
    },
    {
      "parameters": {REMOVED PRIVATE},
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        208,
        0
      ],
      "id": "e592173a-2c47-483d-806e-e835eec2ec99",
      "name": "Edit fields"
    }
  ],
  "connections": {
    "Insert or update rows in a table": {
      "main": [
        [
          {
            "node": "set counter ++",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "getCurrLastQuiz",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "set time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "reset",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "set counter ++": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "fetchProd": {
      "main": [
        [
          {
            "node": "Edit fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "getCurrLastQuiz": {
      "main": [
        [
          {
            "node": "fetchProd",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "reset": {
      "main": [
        [
          {
            "node": "getCurrLastQuiz",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "set time": {
      "main": [
        []
      ]
    },
    "Edit fields": {
      "main": [
        [
          {
            "node": "Insert or update rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "instanceId": "a1e8e16e8251d7e450a77c833c5134c5211d75fa49c6fd2e71206b45ffe53bfb"
  }
}

Share the output returned by the last node

N/A

Information on your n8n setup

  • n8n version:Self-hosted
  • Database (default: SQLite): Postgres
  • n8n EXECUTIONS_PROCESS setting (default: own, main): main
  • Running n8n via (Docker, npm, n8n cloud, desktop app): docker
  • Operating system:linux

Hi @Bogdan_M

I think this is the problem, your condition is clear:

Whenever the fetchProd node output items are greater than 1, the loop continues, which causes the infinite loop..

I tried it with mock data, and this is exactly how your logic behaves..

You could probably try using the Loop Over Items (Split in Batches) node instead..

Hello @mohamed3nan

Thank you for joining my topic!

Could you please elaborate?

Unfortunately I do not understand. I thought it makes sense, to continue fetching them, until there is still data returned from the database. And to stop when there is no data.

And the second question is, how can this loop over items node help me? Can it stop my cycle at some point?

The looping issue is now fully resolved — the workflow correctly exits the cycle once all records are processed.

However, there’s still one persistent problem: when running the full dataset (~50,000 records), the workflow eventually crashes on the MySQL node near the end of execution.

What’s strange is that:

  • All records do get written successfully before it crashes.

  • The same exact logic runs fine with smaller subsets (e.g. 20,000 rows).

  • There are no visible errors in the n8n logs or console — it just dies silently while the MySQL node shows as “running.”

So it seems like some kind of resource exhaustion or timeout happening only under higher load, even though the query itself is valid.

Have you seen similar behavior with large data writes or long-running workflows where a database node crashes silently despite successful execution?

probably..

if this works, then use the split in batch loop with Batch Size 20k, or even better approach to use a subworkflow..

What my batches are 5000 at a time. Or do you mean something different?