NEW: Live arbitrage across 10+ prediction markets.Arbitrage →
← Index
APIOct 15, 2026

Polymarket Heartbeat (2026): Keep WebSockets and Orders Alive

Polymarket Heartbeat (2026): Keep WebSockets and Orders Alive

The Short Answer

Polymarket uses "heartbeat" for two things: keepalive frames that hold a WebSocket open, and an order heartbeat that cancels your resting orders if your trading process goes quiet. The order heartbeat is POST /v1/heartbeats on the CLOB, sent every 5 seconds with the latest heartbeat_id. Once the first one is accepted, going 10 seconds without a valid heartbeat cancels every open order owned by those API credentials. On WebSockets, the market and user channels expect a text PING every 10 seconds and answer PONG, while the sports, PolyBolt and RFQ sockets ping you.

Key Takeaways

  • The order heartbeat is opt-in. Nothing changes until the first heartbeat is accepted, and after that a 10-second gap cancels all open orders for those credentials.
  • Polymarket checks for missed heartbeats every 5 seconds, so cancellation can land up to 5 seconds after the timeout.
  • Every successful heartbeat returns a new heartbeat_id, and a stale ID gets HTTP 400 along with the ID the CLOB expects.
  • Market and user channels need a text PING every 10 seconds, and PolyBolt closes a connection with code 4002 after two missed pongs.
  • Polymarket Perps has a separate dead man's switch, auto-cancel, which you arm with a deadline and keep re-arming.

What is the Polymarket heartbeat?

The word covers two mechanisms that solve different problems. A WebSocket keepalive proves the connection is alive, so the server keeps streaming to it. The order heartbeat proves your trading process is alive, so the exchange can pull your resting orders when it is not. A bot that quotes on Polymarket usually needs both.

MechanismWhereWhat it protectsWhen it stops
Order heartbeatPOST /v1/heartbeats on clob.polymarket.comYour resting CLOB ordersAll open orders for those API credentials are canceled
WebSocket keepaliveEach WebSocket connectionThe stream itselfDepends on the socket, as the table below shows
Perps auto-cancelPATCH /v1/trade/auto-cancel on the Perps APIYour open Perps ordersOrders are canceled at the deadline you set

How does the Polymarket order heartbeat work?

Polymarket's docs describe it as a way "to cancel resting orders automatically if your trading process stops responding." It arrived with the January 2026 API update and works in three steps:

  1. Start. Send {"heartbeat_id": ""} to POST /v1/heartbeats, signed with the L2 headers of the same CLOB API credentials that own the orders you want to protect.
  2. Chain. The response carries a heartbeat_id. Five seconds later, send that ID back. Each success returns a new ID for the next call.
  3. Recover. If an ID is invalid or expired, the CLOB answers HTTP 400 and includes the ID it expected. Sign a new request with that ID and retry.

The timing is strict: "Once the first heartbeat is accepted, the CLOB expects the account to keep sending them; if a valid heartbeat is not received within 10 seconds, all open orders owned by those CLOB API credentials are canceled." The check runs every five seconds, so cancellation can arrive up to five seconds after the timeout. Sending every 5 seconds, as the docs advise, keeps two attempts inside each 10-second window.

In our checks on 3 October 2026, POST /v1/heartbeats and the unversioned POST /heartbeats both answered HTTP 401 with Unauthorized/Invalid api key when called without credentials, so both routes are live and both need L2 authentication. The API reference still lists /heartbeats, which takes no body and returns {"status": "ok"}. Polymarket's order management guide uses the ID-chained /v1/heartbeats, and so does this post.

How do you send Polymarket heartbeats in code?

Neither unified SDK wraps the order heartbeat yet. The guide's TypeScript and Python tabs read "Content coming soon," and @polymarket/client 0.12.0 has no method for it, so call the endpoint directly. This TypeScript loop needs nothing beyond Node's built-in crypto and fetch:

import { createHmac } from 'node:crypto';

const CLOB = 'https://clob.polymarket.com';
const PATH = '/v1/heartbeats';
const creds = {
  address: process.env.POLY_SIGNER_ADDRESS!,
  key: process.env.POLY_API_KEY!,
  secret: process.env.POLY_API_SECRET!,
  passphrase: process.env.POLY_PASSPHRASE!,
};

function l2Headers(body: string): Record<string, string> {
  const timestamp = String(Math.floor(Date.now() / 1000));
  const signature = createHmac('sha256', Buffer.from(creds.secret, 'base64'))
    .update(timestamp + 'POST' + PATH + body)
    .digest('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
  return {
    'Content-Type': 'application/json',
    POLY_ADDRESS: creds.address,
    POLY_API_KEY: creds.key,
    POLY_PASSPHRASE: creds.passphrase,
    POLY_SIGNATURE: signature,
    POLY_TIMESTAMP: timestamp,
  };
}

let heartbeatId = '';

async function sendHeartbeat(retry = true): Promise<void> {
  const body = JSON.stringify({ heartbeat_id: heartbeatId });
  const res = await fetch(CLOB + PATH, { method: 'POST', headers: l2Headers(body), body });
  const data = (await res.json()) as { heartbeat_id?: string };
  if (data.heartbeat_id) heartbeatId = data.heartbeat_id;
  if (res.status === 400 && retry) return sendHeartbeat(false); // stale ID: retry once with the expected one
  if (!res.ok) throw new Error(`Heartbeat failed with HTTP ${res.status}`);
}

await sendHeartbeat();
setInterval(() => {
  sendHeartbeat().catch((err) => console.error(err));
}, 5_000);

The signature is HMAC-SHA256 over the timestamp, method, path and exact body, keyed with your base64-decoded API secret and encoded as URL-safe base64 with padding. We compared this function with the signer inside @polymarket/client 0.12.0 on 2,000 random inputs, and the outputs matched every time. Run the loop in the same process that places your orders. If a separate process sends the heartbeats, your orders outlive a crash of the trading code, which defeats the point. Creating the key, secret and passphrase is covered in Polymarket API keys explained.

What happens when the heartbeat stops?

For the order heartbeat, the answer is documented and blunt: every open order owned by those CLOB API credentials is canceled, 10 to 15 seconds after the last valid heartbeat. Stream updates do not replay what you missed, so after reconnecting, Polymarket's docs say to fetch your open orders and recent trades before acting on new events. Trading involves risk, and the heartbeat only limits how long stale quotes can rest. It does not undo fills that happened before the cancel.

For WebSockets, the result depends on the socket:

SocketHostKeepalive ruleDocumented result if it stops
CLOB market channelwss://ws-subscriptions-clob.polymarket.com/ws/marketSend text PING every 10 seconds, the server answers PONGNo timeout is documented
CLOB user channelwss://ws-subscriptions-clob.polymarket.com/ws/userThe same PING rule, and subscribe right after connectingThe server may close a connection left unsubscribed
Sportswss://sports-api.polymarket.com/wsThe server pings, and you reply pong within 10 secondsThe server closes the connection
PolyBolt priceswss://ws-live-v2.polymarket.com/wsThe server sends a protocol ping every 25 secondsTwo missed pongs close it with code 4002
Combos RFQwss://combos-rfq-gateway-quoter.polymarket.com/ws/rfqProtocol ping every 30 seconds with payload rfq, echoed in your pongClosed after 2 minutes without an inbound message or pong
Perpswss://ws.perpetuals.polymarket.com/v1/wsSend an application ping about every 25 secondsTreat it as stale after about 65 seconds of silence

PolyBolt pairs its close codes with retry advice. On 4002, reconnect with exponential backoff and jitter, then authenticate again and resend your subscriptions. Sequence numbers start over on the new connection. A 4003 means the server is draining, so reconnect after a random delay of 0 to 10 seconds. In our checks, PolyBolt's protocol pings arrived 25 seconds apart, and an application-level {"op":"ping","rid":"p1"} came back as {"op":"pong","rid":"p1"}.

How do you keep the market and user channels alive?

These two channels use an application-level heartbeat, not WebSocket protocol pings. You send the literal text PING, and the server answers with a bare PONG. This script holds a market channel open and answers the sports socket, using Node's built-in WebSocket client. We ran it on Node 24:

const TOKEN_ID = 'YOUR_TOKEN_ID';

// Market channel: you send PING every 10 seconds and the server answers PONG
const market = new WebSocket('wss://ws-subscriptions-clob.polymarket.com/ws/market');
let pingTimer;

market.addEventListener('open', () => {
  market.send(JSON.stringify({ assets_ids: [TOKEN_ID], type: 'market' }));
  pingTimer = setInterval(() => market.send('PING'), 10_000);
});
market.addEventListener('message', ({ data }) => {
  if (data === 'PONG') return; // heartbeat reply, not JSON
  const event = JSON.parse(data);
  // handle book, price_change, last_trade_price and tick_size_change events
});
market.addEventListener('close', () => clearInterval(pingTimer));

// Sports channel: the server pings you, so answer a text ping with pong
const sports = new WebSocket('wss://sports-api.polymarket.com/ws');
sports.addEventListener('message', ({ data }) => {
  if (data === 'ping') return sports.send('pong');
  const game = JSON.parse(data);
});

Check for PONG before JSON.parse, because it is not JSON. In our run on 3 October 2026, the market channel answered all three PINGs at 10-second intervals while 1,035 other messages streamed through. The user channel works the same way, with your CLOB credentials in the subscription frame. More on the market channel is in the Polymarket WebSocket API guide.

One caution on sports. Polymarket's docs say the server sends a text ping every 5 seconds, but in our checks it sent standard WebSocket ping frames every 15 seconds instead, which built-in clients answer automatically. With automatic pongs switched off, the server dropped our connection at 45 seconds, so leave protocol pongs on and keep the text reply too.

Does Polymarket Perps have a dead man's switch?

Yes, and it works differently. Perps auto-cancel is "a dead man's switch for your open orders." You arm it with a deadline at least five seconds in the future, and the exchange cancels every open order on the account when that deadline passes. Keep re-arming it while your process runs. Polymarket's example arms a 60-second deadline and re-arms every 20 seconds. The switch fires once and then clears itself, so orders placed afterwards are unprotected until you arm it again.

Over the API it is PATCH /v1/trade/auto-cancel on api.perpetuals.polymarket.com, with a time of 0 to disarm, and GET /v1/account/auto-cancel reports the deadline and trigger count. The TypeScript SDK wraps it as session.armAutoCancel(), disarmAutoCancel() and fetchAutoCancelStatus(), and the Python SDK uses the snake_case equivalents. An account can trigger it up to 1,000 times per UTC day, so disarm on a clean shutdown instead of letting it fire.

How do you manage connections across venues?

Every venue brings its own sockets, keepalive rules and cancel logic, and a bot that trades beyond Polymarket inherits all of them. The Predictefy API streams order books and trades from 15+ venues over its WebSocket streams, and orders are signed in your own wallet or process, so Predictefy never holds your funds or keys. Paper trading costs 0 credits on every plan, which makes it a good place to rehearse how your bot handles open orders before real ones rest on a book.

Frequently Asked Questions

What is the Polymarket heartbeat endpoint?

It is POST /v1/heartbeats on the CLOB at clob.polymarket.com, authenticated with L2 headers. Send it every 5 seconds with the latest heartbeat_id. Once the first heartbeat is accepted, a 10-second gap cancels all open orders owned by those API credentials.

What happens if I stop sending Polymarket heartbeats?

All open orders owned by those CLOB API credentials are canceled once 10 seconds pass without a valid heartbeat. The check runs every 5 seconds, so the cancel can land up to 5 seconds later. This only applies after your first heartbeat has been accepted.

How often should I send PING on the Polymarket WebSocket?

Every 10 seconds on the market and user channels, as the literal text PING. The server answers with PONG. It is an application-level heartbeat, so send it as a text frame rather than relying on your library's protocol pings.

Why did I get "Invalid Heartbeat ID"?

You sent an old or wrong heartbeat_id. The HTTP 400 response includes the ID the CLOB expected, so sign a new request with that ID and retry right away to stay inside the 10-second window.

Does the Polymarket SDK send order heartbeats for me?

Not yet. Polymarket's heartbeat guide shows its TypeScript and Python tabs as content coming soon, and @polymarket/client 0.12.0 has no heartbeat method, so call POST /v1/heartbeats directly.

What does WebSocket close code 4002 mean on Polymarket?

On the PolyBolt price socket, 4002 means a slow consumer or a pong timeout, such as two missed pongs. Reconnect with exponential backoff and jitter, authenticate again, resend your subscriptions, and reduce subscription load if it keeps happening.