The Arbitrum sequencer went silent for roughly an hour in late 2023. Optimism has had similar incidents. Base has had brief hiccups. When a sequencer stops accepting transactions, payments submitted against it hang in a state that is neither confirmed nor failed — the worst possible outcome for a checkout flow. The user sees nothing. The merchant sees nothing. Your internal monitoring sees a transaction that was never sequenced.
This post covers three things: how to detect a sequencer outage fast, how to force-include transactions through L1 when the sequencer is unavailable, and how to design your payment gateway's state machine to handle both the outage and the recovery window.
Why this failure mode is nastier than a normal node outage#
When an RPC node fails, you get connection errors, timeouts, or stale responses. You already have failover logic for that — switch to a secondary RPC, retry the submission, and the transaction either lands or you get an explicit rejection.
When the sequencer itself is down, the L1 contracts are still functioning normally. The chain's canonical state keeps advancing via the batch-posting and fraud-proof window. But no new transactions are being ordered. Your transactions queue in your local mempool, in the user's wallet, or get rejected by the sequencer with a rate-limit or timeout error that is easy to misread as a transient RPC glitch.
The particularly nasty variant: a sequencer that accepts your transaction but
stops publishing batches. The transaction appears pending to your local node,
which is still processing the sequencer's last known state. You never see a
rejection; you just never see a receipt.
Detecting a sequencer outage#
Chainlink L2 Sequencer Uptime Feeds#
Chainlink publishes a Sequencer Uptime Feed on every major EVM L2. These are
on-chain Aggregator contracts reporting whether the sequencer is up or down,
updated within seconds of a confirmed outage by the oracle network.
Contract addresses (verified against Chainlink docs, April 2026):
| Chain | Sequencer Uptime Feed |
|---|---|
| Arbitrum One | 0xFdB631F5EE196F0ed6FAa767959853A9F217697D |
| Optimism | 0x371EAD81c9102C9BF4874A9075FFFf170F2D5C68 |
| Base | 0xBCF85224fc0756B9Fa45aA7892530B47e10b6082 |
The feed returns a standard latestRoundData() response. The answer field
is 0 when the sequencer is up, 1 when it is down. The startedAt timestamp
records when the current status was first reported — critical for distinguishing
a fresh outage from one that has been running for two hours.
import { createPublicClient, http, parseAbi } from "viem";
import { arbitrum } from "viem/chains";
const SEQUENCER_FEED = "0xFdB631F5EE196F0ed6FAa767959853A9F217697D";
const FEED_ABI = parseAbi([
"function latestRoundData() external view returns " +
"(uint80 roundId, int256 answer, uint256 startedAt, " +
"uint256 updatedAt, uint80 answeredInRound)",
]);
// IMPORTANT: use an L1 RPC here, not the L2 RPC.
// If the L2 sequencer is down, the L2 RPC may be unreachable.
const l1Client = createPublicClient({
chain: arbitrum,
transport: http(process.env.L1_RPC_URL),
});
async function isSequencerDown(): Promise<{ down: boolean; since: Date }> {
const [, answer, startedAt] = await l1Client.readContract({
address: SEQUENCER_FEED,
abi: FEED_ABI,
functionName: "latestRoundData",
});
return {
down: answer === 1n,
since: new Date(Number(startedAt) * 1000),
};
}The grace period after recovery#
Chainlink's own documentation recommends a grace period of 3,600 seconds after a sequencer comes back online before you re-enable dependent logic. For payment gateways, a 10-to-20 minute window balances caution with user experience: L2 nodes need time to re-sync the sequencer's backlog, and oracle price feeds that paused during the outage need to re-warm.
Scale the grace period to your transaction value floor: a $50 checkout can accept 10 minutes; a $50,000 treasury sweep should wait the full hour.
const GRACE_SECONDS = 15 * 60; // 15 minutes
async function isSequencerSafe(): Promise<boolean> {
const { down, since } = await isSequencerDown();
if (down) return false;
const secondsSinceRecovery = (Date.now() - since.getTime()) / 1000;
return secondsSinceRecovery >= GRACE_SECONDS;
}Forced inclusion: the L1 escape hatch#
Every optimistic rollup with a correctly designed trust model must allow users to bypass the sequencer and submit transactions directly to L1. Without this, a malicious or offline sequencer could censor transactions permanently. Both Arbitrum and the OP Stack implement forced inclusion, with different mechanics and very different latency profiles.
Arbitrum: the Delayed Inbox#
Arbitrum exposes a Inbox contract on Ethereum mainnet. Calling
sendL2Message on it enqueues your transaction in the Delayed Inbox. The
sequencer is required by protocol to include it within the "force-inclusion
timeout" — currently around 24 hours for Arbitrum One. After that window
passes without inclusion, anyone can call forceInclusion to push it through.
This is not fast. Forced inclusion is a censorship-resistance backstop, not a latency optimization. For a payment gateway, the relevant scenario is: the sequencer has been down long enough that you need a guarantee the transaction eventually lands rather than being silently lost.
import { parseEther, parseAbi } from "viem";
// Arbitrum Inbox on Ethereum mainnet (Arbitrum One)
const L1_INBOX = "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f";
const INBOX_ABI = parseAbi([
"function sendL2Message(bytes calldata messageData) external returns (uint256)",
]);
/**
* encodedL2Tx: RLP-encoded L2 transaction bytes
* (use @arbitrum/sdk ArbitrumProvider.getRawTransaction to produce this)
*/
async function submitViaDelayedInbox(
l1WalletClient: any,
encodedL2Tx: `0x${string}`
): Promise<`0x${string}`> {
return l1WalletClient.writeContract({
address: L1_INBOX,
abi: INBOX_ABI,
functionName: "sendL2Message",
args: [encodedL2Tx],
value: parseEther("0.001"), // L1 gas donation included with the message
});
}Optimism and Base: OptimismPortal.depositTransaction#
The OP Stack uses OptimismPortal.depositTransaction() for L1-forced
inclusion. Unlike Arbitrum's 24-hour window, OP Stack deposits are required to
appear in the next available L2 block — typically within 1-2 minutes. The
mechanism is faster but uses a separate deposit queue with different gas
semantics from regular L2 transactions.
// OptimismPortal on Ethereum mainnet (OP Mainnet)
const OP_PORTAL = "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed";
const PORTAL_ABI = parseAbi([
"function depositTransaction(" +
"address _to, uint256 _value, uint64 _gasLimit, " +
"bool _isCreation, bytes calldata _data) external payable",
]);
async function forceIncludeOnOptimism(
l1WalletClient: any,
to: `0x${string}`,
calldata: `0x${string}`,
gasLimit: bigint
): Promise<`0x${string}`> {
return l1WalletClient.writeContract({
address: OP_PORTAL,
abi: PORTAL_ABI,
functionName: "depositTransaction",
args: [to, 0n, gasLimit, false, calldata],
value: 0n,
});
}For Base, swap OP_PORTAL for Base's portal address
(0x49048044D57e1C92A77f79988d21Fa8fAF74E97e on mainnet) — the ABI is
identical.
Payment gateway state machine#
Normal L2 payment flows have two states: pending and confirmed. A
sequencer-aware gateway needs at least six:
type L2PaymentStatus =
| "pending" // submitted to sequencer, awaiting receipt
| "sequencer_down" // outage detected before or during submission
| "pending_grace_period" // sequencer recovered, within the cooldown window
| "forced_inclusion" // submitted to L1 inbox/portal, awaiting L2 inclusion
| "confirmed" // L2 tx mined with sufficient confirmations
| "expired"; // outage exceeded payment TTL; re-issue requiredThe transition rules:
pending ──── sequencer detected down ──► sequencer_down
│
outage > payment_ttl ─────┼──► expired
│
sequencer recovers ───────────►│
pending_grace_period
│
grace window ends ────────────┼──► forced_inclusion ──► confirmed
│
(re-queue if grace window ends) ─────┘
The expired state is important. If the outage lasts longer than your
payment's time-to-live — typically 15-30 minutes for a checkout session — the
merchant experience is cleaner if you expire the payment and issue a new one
when the sequencer recovers, rather than force-including a stale transaction
that may reference outdated pricing or an abandoned user session.
const FORCE_INCLUDE_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
const PAYMENT_TTL_MS = 20 * 60 * 1000; // 20 minutes
async function handlePendingPayment(payment: Payment): Promise<void> {
const outageMs = Date.now() - payment.sequencerDownAt.getTime();
if (outageMs >= PAYMENT_TTL_MS) {
await updatePaymentStatus(payment.id, "expired");
return;
}
if (outageMs >= FORCE_INCLUDE_THRESHOLD_MS && !payment.forcedInclusionQueued) {
const l1Hash = await submitViaDelayedInbox(l1Client, payment.encodedTx);
await updatePaymentStatus(payment.id, "forced_inclusion", { l1Hash });
}
}BchainPay API integration#
BchainPay surfaces sequencer state through the payment object's
l2_sequencer field:
{
"id": "pay_3Qx8kWrnT9aM",
"status": "forced_inclusion",
"chain": "arbitrum",
"l2_sequencer": {
"status": "down",
"down_since": "2026-04-27T14:12:05Z",
"force_inclusion_queued": true,
"l1_tx_hash": "0xabc123…",
"estimated_inclusion_at": "2026-04-27T14:22:00Z"
}
}Webhook event payment.sequencer_state_changed fires on each status
transition. Subscribe to it to drive frontend updates — replace the generic
"Waiting for confirmation" spinner with "Sequencer outage detected. Your
payment is secured on L1 and will process shortly." once
force_inclusion_queued is true. A factual status message during an outage
converts a support ticket into a handled experience.
To query current L2 health across all chains:
curl https://api.bchainpay.com/v1/network/health \
-H "Authorization: Bearer $BCHAINPAY_KEY" \
| jq '.l2s[] | {chain, sequencer_status, down_since}'Monitoring setup#
Poll the Chainlink feed every 30 seconds from your backend using a dedicated L1 RPC — not from the L2 RPC, which may be unreachable during the outage itself. This is the most common operational mistake: teams that check sequencer status via the L2 endpoint see the check itself time out and mistake it for a connectivity problem rather than a sequencer problem.
# Cron probe, 30-second interval
0/1 * * * * curl -sf https://internal.bchainpay.com/probe/l2-sequencer \
| grep -q '"down":true' && alert pagerduty "l2_sequencer_down"Configure two alert thresholds:
- Warn at 2 minutes of confirmed downtime — enough to filter false positives from brief oracle update gaps.
- Page at 5 minutes — activates the forced-inclusion pipeline and triggers user-facing status page updates.
The mean time to recovery across documented Arbitrum and Optimism incidents has been under 45 minutes. A payment gateway SLA of "settlement within 90 minutes" is achievable with forced inclusion as the backstop. "Within 5 minutes" is not, and should be disclosed as dependent on sequencer liveness in your merchant agreement.
What we got wrong#
A few things we had to learn after shipping:
-
Checking sequencer status via the L2 RPC. We initially polled the Chainlink feed via the Arbitrum RPC. When the sequencer went down, the feed check timed out, and our alert fired as "RPC node down" not "sequencer down" — two entirely different runbooks. The fix: route all sequencer health checks through a dedicated L1 endpoint.
-
Not enforcing the grace period. After the sequencer recovered, we re-enabled payments immediately. Two merchants had in-flight ERC-20 transfers that referenced a stale oracle price from before the outage. The grace period exists for this reason: wait for oracle feeds to re-warm, not just for the sequencer to come back online.
-
Exposing "forced inclusion" as a status to end users. Merchants do not know what this means. The right UX is a plain-English status message and a realistic ETA. The internal state machine label is for your engineers.
-
Using the same
PAYMENT_TTLfor all payment types. Subscription renewals and high-value B2B invoices can have a 2-hour TTL without degrading UX. Checkout sessions should expire in 15-20 minutes. Do not use a single constant.
Key takeaways#
- L2 sequencer outages are a production reality. Every major optimistic rollup has logged at least one multi-minute incident.
- Chainlink Sequencer Uptime Feeds are the fastest on-chain detection signal. Poll them on a 30-second cycle, always via an L1 RPC — not the L2 RPC.
- Enforce a 10-to-20 minute grace period after a sequencer comes back online. Oracle price feeds need time to re-warm; payments submitted immediately after recovery can reference stale state.
- Arbitrum's Delayed Inbox provides a 24-hour inclusion guarantee at the cost
of high latency. OP Stack's
depositTransactionis faster (1-2 minutes) but uses a separate deposit queue. - Model at least six payment states:
pending,sequencer_down,pending_grace_period,forced_inclusion,confirmed, andexpired. - Expire short-TTL payments rather than force-including them if the outage exceeds the session window. A fresh payment after recovery is cleaner than a stale forced-inclusion.
- Never check sequencer status through the L2 RPC. If the sequencer is down, the RPC check will fail for a different reason and fire the wrong alert.